Repository: Altinn/altinn-authorization-tmp
Pull request #684: issues(654): Problem Details
Author: login: andreasisnes
Created: 2025-05-06T11:32:31Z | Merged: 2025-05-13T10:45:37Z

## Verification
- [ ] **Your** code builds clean without any errors or warnings
- [ ] Manual testing done (required)
- [ ] Relevant automated test added (if you find this hard, leave it and we'll help out)
- [ ] All tests run green

## Documentation
- [ ] User documentation is updated with a separate linked PR in [altinn-studio-docs.](https://github.com/Altinn/altinn-studio-docs) (if applicable)



Diff:
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/AccessManagementEnduserHost.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/AccessManagementEnduserHost.cs
index aed54d003..e78f2aee7 100644
--- a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/AccessManagementEnduserHost.cs
+++ b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/AccessManagementEnduserHost.cs
@@ -1,6 +1,4 @@
-using Altinn.AccessManagement.Api.Enduser.Mappers;
-using Altinn.AccessManagement.Api.Enduser.Models;
-using Altinn.AccessMgmt.Core.Models;
+using Altinn.AccessManagement.Enduser.Services;
 using Altinn.Authorization.Host.Startup;
 
 namespace Altinn.AccessManagement.Api.Enduser;
@@ -22,7 +20,7 @@ public static partial class AccessManagementEnduserHost
     public static IHostApplicationBuilder AddAccessManagementEnduser(this IHostApplicationBuilder builder)
     {
         Log.AddHost(Logger);
-        builder.Services.AddSingleton<IMapper<AssignmentExternal, Assignment>, AssignmentExternalMapper>();
+        builder.Services.AddSingleton<IEnduserConnectionService, ConnectionService>();
         return builder;
     }
 
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Altinn.AccessManagement.Api.Enduser.csproj b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Altinn.AccessManagement.Api.Enduser.csproj
index 9b54d6232..dfc8d7ea3 100644
--- a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Altinn.AccessManagement.Api.Enduser.csproj
+++ b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Altinn.AccessManagement.Api.Enduser.csproj
@@ -1,5 +1,4 @@
 <Project Sdk="Microsoft.NET.Sdk.Web">
-
     <ItemGroup>
         <PackageReference Include="Altinn.Common.PEP" />
         <PackageReference Include="Microsoft.AspNetCore.OpenApi" />
@@ -7,6 +6,9 @@
     </ItemGroup>
 
     <ItemGroup>
+        <PackageReference Include="Swashbuckle.AspNetCore" />
+        <PackageReference Include="Swashbuckle.AspNetCore.Annotations" />
+        <PackageReference Include="Swashbuckle.AspNetCore.Filters" />
         <ProjectReference Include="..\Altinn.AccessManagement.Core\Altinn.AccessManagement.Core.csproj" />
         <ProjectReference Include="..\Altinn.AccessMgmt.Persistence\Altinn.AccessMgmt.Persistence.csproj" />
     </ItemGroup>
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/AccessPartiesController.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/AccessPartiesController.cs
deleted file mode 100644
index 73930955e..000000000
--- a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/AccessPartiesController.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-using System.Net.Mime;
-using Altinn.AccessManagement.Api.Enduser.Mappers;
-using Altinn.AccessManagement.Api.Enduser.Models;
-using Altinn.AccessManagement.Core.Constants;
-using Altinn.AccessManagement.Core.Extensions;
-using Altinn.AccessManagement.Core.Filters;
-using Altinn.AccessMgmt.Core.Models;
-using Altinn.AccessMgmt.Persistence.Core.Models;
-using Altinn.AccessMgmt.Persistence.Data;
-using Altinn.AccessMgmt.Persistence.Services.Contracts;
-using Altinn.Authorization.ProblemDetails;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.FeatureManagement.Mvc;
-
-namespace Altinn.AccessManagement.Api.Enduser.Controllers;
-
-/// <summary>
-/// Controller for end user api operations for parties which have provided access to the user or it's organizations.
-/// </summary>
-[ApiController]
-[Route("accessmanagement/api/v1/enduser/access/parties")]
-[FeatureGate(AccessManagementEnduserFeatureFlags.ControllerAccessParties)]
-[Authorize(Policy = AuthzConstants.SCOPE_PORTAL_ENDUSER)]
-public class AccessPartiesController(IHttpContextAccessor accessor, IAssignmentService assignmentService, IMapper<AssignmentExternal, Assignment> mapper) : ControllerBase
-{
-    private IHttpContextAccessor Accessor { get; } = accessor;
-
-    private IAssignmentService AssignmentService { get; } = assignmentService;
-
-    private IMapper<AssignmentExternal, Assignment> Mapper { get; } = mapper;
-
-    /// <summary>
-    /// Get access parties
-    /// </summary>
-    /// <returns></returns>
-    [HttpGet]
-    public async Task<ActionResult> GetAccessParties()
-    {
-        return await Task.FromResult(Ok());
-    }
-
-    /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
-    /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
-    [HttpPost]
-    [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
-    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
-    [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> PostAccessParty([FromQuery] Guid party, [FromQuery] Guid to, CancellationToken cancellationToken = default)
-    {
-        var partyUuid = Accessor.GetPartyUuid();
-        var audit = new ChangeRequestOptions()
-        {
-            ChangedBy = partyUuid,
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        var assignment = await AssignmentService.GetOrCreateAssignment(party, to, "rightholder", audit, cancellationToken);
-
-        if (assignment.IsProblem)
-        {
-            return assignment.Problem.ToActionResult();
-        }
-
-        return Ok(Mapper.Map(assignment.Value));
-    }
-
-    /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
-    /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cascade">Must be set to true if enduser should delete active foreign connections.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
-    [HttpDelete]
-    [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType(StatusCodes.Status204NoContent)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
-    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
-    [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> DeleteAccessParty([FromQuery] Guid party, [FromQuery] Guid to, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
-    {
-        var partyUuid = Accessor.GetPartyUuid();
-        var audit = new ChangeRequestOptions()
-        {
-            ChangedBy = partyUuid,
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        var problem = await AssignmentService.DeleteAssignment(party, to, "rightholder", audit, cascade, cancellationToken);
-
-        if (problem is { })
-        {
-            return problem.ToActionResult();
-        }
-
-        return NoContent();
-    }
-}
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
index 11fb9c841..d5cd19466 100644
--- a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
+++ b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
@@ -1,16 +1,16 @@
 ﻿using System.Net.Mime;
 using Altinn.AccessManagement.Api.Enduser.Models;
 using Altinn.AccessManagement.Core.Constants;
-using Altinn.AccessManagement.Core.Extensions;
-using Altinn.AccessManagement.Core.Filters;
+using Altinn.AccessManagement.Core.Errors;
+using Altinn.AccessManagement.Core.Models;
+using Altinn.AccessManagement.Enduser.Services;
+using Altinn.AccessMgmt.Core.Models;
 using Altinn.AccessMgmt.Persistence.Core.Models;
 using Altinn.AccessMgmt.Persistence.Data;
-using Altinn.AccessMgmt.Persistence.Repositories.Contracts;
-using Altinn.AccessMgmt.Persistence.Services.Contracts;
+using Altinn.AccessMgmt.Persistence.Services;
 using Altinn.Authorization.ProblemDetails;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Options;
 using Microsoft.FeatureManagement.Mvc;
 
 namespace Altinn.AccessManagement.Api.Enduser.Controllers;
@@ -22,115 +22,63 @@ namespace Altinn.AccessManagement.Api.Enduser.Controllers;
 [Route("accessmanagement/api/v1/enduser/connections")]
 [FeatureGate(AccessManagementEnduserFeatureFlags.ControllerConnections)]
 [Authorize(Policy = AuthzConstants.SCOPE_PORTAL_ENDUSER)]
-public class ConnectionController(IHttpContextAccessor accessor, IConnectionService connectionService, IAssignmentService assignmentService, IEntityRepository entityRepository) : ControllerBase
+public class ConnectionController(IEnduserConnectionService connectionService) : ControllerBase
 {
-    private IHttpContextAccessor Accessor { get; } = accessor;
+    private IEnduserConnectionService ConnectionService { get; } = connectionService;
 
     /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
+    /// Get connections between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpGet]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<Assignment>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetConnections([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
-        {
-            return BadRequest();
-        }
-
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
-        {
-            // Party must match From or To
-            return BadRequest();
-        }
-
-        var partyUuid = Accessor.GetPartyUuid();
-        var audit = new ChangeRequestOptions()
-        {
-            ChangedBy = partyUuid,
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from.HasValue && to.HasValue)
+        if (ValidationRules.EnduserReadConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Ok(await connectionService.Get(fromId: from.Value, toId: to.Value, facilitatorId: null, cancellationToken: cancellationToken));
+            return problem.ToActionResult();
         }
 
-        if (from.HasValue)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        var result = await ConnectionService.Get(fromUuid, toUuid, cancellationToken: cancellationToken);
+        if (result.IsProblem)
         {
-            return Ok(await connectionService.GetReceived(from.Value, cancellationToken: cancellationToken));
+            return result.Problem.ToActionResult();
         }
 
-        if (to.HasValue)
-        {
-            return Ok(await connectionService.GetGiven(to.Value, cancellationToken: cancellationToken));
-        }
-        
-        return BadRequest();
+        return Ok(PaginatedResult.Create(result.Value, null));
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
     [HttpPost]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, CancellationToken cancellationToken = default)
+    [ProducesResponseType<Assignment>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddAssignment([FromQuery] ConnectionInput connection, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from != party)
-        {
-            throw new Exception("From party does not match from");
-        }
-
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
-        {
-            return Problem("From party not found");
-        }
-
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("From must be of type 'Organisasjon'");
-        }
-
-        //// To must be Type:Organisasjon
-        //// #550:AC:Det skal bare være mulig å legge til Organisasjoner som ny Rightholder
-        var toEntity = await entityRepository.GetExtended(to);
-        if (toEntity == null)
+        if (ValidationRules.EnduserAddConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Problem("To party not found");
+            return problem.ToActionResult();
         }
 
-        if (!toEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        var result = await ConnectionService.AddAssignment(fromUuid, toUuid, "rettighetshaver", cancellationToken);
+        if (result.IsProblem)
         {
-            return Problem("To must be of type 'Organisasjon'");
+            return result.Problem.ToActionResult();
         }
 
-        var res = await assignmentService.GetOrCreateAssignmentInternal(fromId: from, toId: to, roleCode: "rettighetshaver", options, cancellationToken: cancellationToken);
-
-        if (res != null)
-        {
-            return Ok(res);
-        }
-
-        return Problem("Unable add connection");
+        return Ok(result.Value);
     }
 
     /// <summary>
@@ -138,165 +86,120 @@ public async Task<IActionResult> AddConnection([FromQuery] Guid party, [FromQuer
     /// </summary>
     [HttpDelete]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<ProblemInstance> RemoveConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> RemoveAssignment([FromQuery] ConnectionInput connection, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (!(from == party) && !(to == party))
+        if (ValidationRules.EnduserRemoveConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            throw new Exception("From party does not match from or to");
+            return problem.ToActionResult();
         }
 
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        problem = await ConnectionService.RemoveAssignment(fromUuid, toUuid, "rettighetshaver", cascade, cancellationToken);
+        if (problem is { })
         {
-            throw new Exception("From party not found");
+            return problem.ToActionResult();
         }
 
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            throw new Exception("From must be of type 'Organisasjon'");
-        }
-
-        return await assignmentService.DeleteAssignment(fromId: from, toId: to, roleCode: "rettighetshaver", options, cascade: cascade, cancellationToken);
+        return NoContent();
     }
 
     /// <summary>
     /// Creates an assignment between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
-    [HttpGet]
-    [Route("accesspackages")]
+    [HttpGet("accesspackages")]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [ProducesResponseType<PaginatedResult<ConnectionPackage>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetPackages([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetPackages([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
+        if (ValidationRules.EnduserReadConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        var result = await ConnectionService.GetPackages(fromUuid, toUuid, cancellationToken);
+        if (result.IsProblem)
         {
-            // Party must match From or To
-            return BadRequest();
+            return result.Problem.ToActionResult();
         }
 
-        var res = await connectionService.GetPackages(fromId: from, toId: to);
-
-        return Ok(res);
+        return Ok(PaginatedResult.Create(result.Value, null));
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
-    [HttpPost]
-    [Route("accesspackages")]
+    [HttpPost("accesspackages")]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddPackages([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] Guid? package, [FromQuery] string packageUrn, CancellationToken cancellationToken = default)
+    [ProducesResponseType<AssignmentPackage>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddPackages([FromQuery] ConnectionInput connection, [FromQuery] Guid? packageId, [FromQuery] string packageUrn, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from != party)
-        {
-            // Party must match From or To
-            return BadRequest();
-        }
-
-        //// From must by Type:Organisasjon
-        //// #568:AC:From party må være en Organisasjon (skal ikke være mulig å delegere fra privatperson el. andre entitetstyper enda)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
-        {
-            throw new Exception("From party not found");
-        }
-
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
+        if (ValidationRules.EnduserAddConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            throw new Exception("From must be of type 'Organisasjon'");
+            return problem.ToActionResult();
         }
 
-        //// To must be Type:Organisasjon
-        //// #568:AC:To party må være en Organisasjon (skal ikke være mulig å delegere fra privatperson el. andre entitetstyper enda)
-        var toEntity = await entityRepository.GetExtended(to);
-        if (toEntity == null)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        async Task<Result<AssignmentPackage>> AddPackage()
         {
-            return Problem("To party not found");
-        }
-
-        if (!toEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("To must be of type 'Organisasjon'");
-        }
-
-        if (package.HasValue)
-        {
-            var res = await connectionService.AddPackage(fromId: from, toId: to, roleCode: "rettighetshaver", packageId: package.Value, options);
-            if (res)
+            if (packageId.HasValue)
             {
-                return Ok();
+                return await ConnectionService.AddPackage(fromUuid, toUuid, "rettighetshaver", packageId.Value, cancellationToken);
             }
+
+            return await ConnectionService.AddPackage(fromUuid, toUuid, "rettighetshaver", packageUrn, cancellationToken);
         }
-        else
+
+        var result = await AddPackage();
+        if (result.IsProblem)
         {
-            packageUrn = packageUrn.ToLower().StartsWith("urn:") ? packageUrn : ":" + packageUrn;
-            var res = await connectionService.AddPackage(fromId: from, toId: to, roleCode: "rettighetshaver", packageUrn: packageUrn, options);
-            if (res)
-            {
-                return Ok();
-            }
+            return result.Problem.ToActionResult();
         }
 
-        return Problem("Unable to remove package");
+        return Ok(result.Value);
     }
 
     /// <summary>
     /// Remove package from connection (assignment or delegation)
     /// </summary>
-    [HttpDelete]
-    [Route("accesspackages")]
+    [HttpDelete("accesspackages")]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> RemovePackages([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] Guid package, CancellationToken cancellationToken = default)
+    [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> RemovePackages([FromQuery] ConnectionInput connection, [FromQuery] Guid package, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (!(from == party) && !(to == party))
+        if (ValidationRules.EnduserRemoveConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            // Party must match From or To
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        var res = await connectionService.RemovePackage(fromId: from, toId: to, roleCode: "rettighetshaver", packageId: package, options);
-
-        if (res)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        problem = await ConnectionService.RemovePackage(fromUuid, toUuid, "rettighetshaver", package, cancellationToken);
+        if (problem is { })
         {
-            return Ok();
+            return problem.ToActionResult();
         }
 
-        return Problem("Unable to remove package");
+        return NoContent();
     }
 }
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/EnduserValidationRules.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/EnduserValidationRules.cs
new file mode 100644
index 000000000..8d6c60239
--- /dev/null
+++ b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/EnduserValidationRules.cs
@@ -0,0 +1,538 @@
+using Altinn.AccessMgmt.Core.Models;
+using Altinn.Authorization.ProblemDetails;
+
+namespace Altinn.AccessManagement.Core.Errors;
+
+/// <summary>
+/// A utility class that provides methods for validating data using a series of rules.
+/// </summary>
+public static class ValidationRules
+{
+    /// <summary>
+    /// A delegate representing a validation rule that accepts a reference to a <see cref="ValidationErrorBuilder"/>
+    /// and adds errors if validation fails.
+    /// </summary>
+    /// <param name="errors">The reference to the <see cref="ValidationErrorBuilder"/> where validation errors are added.</param>
+    public delegate void Func(ref ValidationErrorBuilder errors);
+
+    /// <summary>
+    /// A delegate that returns a validation rule that accepts a reference to a <see cref="ValidationErrorBuilder"/>
+    /// and adds errors if validation fails.
+    /// </summary>
+    public delegate Func FuncExpression();
+
+    /// <summary>
+    /// Validates a series of rules against the provided parameters.
+    /// It executes each rule in sequence and returns a <see cref="ValidationProblemInstance"/>
+    /// if any validation errors are found, or <c>null</c> if no errors exist.
+    /// </summary>
+    /// <param name="rules">An array of validation rules to apply.</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> containing validation errors, if any,
+    /// or <c>null</c> if the validation passed without any errors.
+    /// </returns>
+    public static ValidationProblemInstance? Validate(params FuncExpression[] rules)
+    {
+        var builder = default(ValidationErrorBuilder);
+        All(rules)()(ref builder);
+        builder.TryBuild(out var result);
+        return result;
+    }
+
+    /// <summary>
+    /// Validates a series of rules against the provided parameters.
+    /// It executes each rule in sequence and returns a <see cref="ValidationProblemInstance"/>
+    /// if any validation errors are found, or <c>null</c> if no errors exist.
+    /// </summary>
+    /// <param name="rules">An array of validation rules to apply.</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> containing validation errors, if any,
+    /// or <c>null</c> if the validation passed without any errors.
+    /// </returns>
+    public static ValidationProblemInstance? Validate(params Func[] rules)
+    {
+        var builder = default(ValidationErrorBuilder);
+        foreach (var rule in rules)
+        {
+            rule(ref builder);
+        }
+
+        builder.TryBuild(out var result);
+
+        return result;
+    }
+
+    /// <summary>
+    /// Combines multiple validation rules that must all pass.
+    /// </summary>
+    /// <param name="funcs">The validation functions to combine.</param>
+    /// <returns>A combined validation rule that applies all the specified rules.</returns>
+    public static FuncExpression All(params FuncExpression[] funcs) => () =>
+    {
+        var results = new List<Func>();
+        foreach (var func in funcs)
+        {
+            if (func() is var fn && fn is { })
+            {
+                results.Add(fn);
+            }
+        }
+
+        return (ref ValidationErrorBuilder errors) =>
+        {
+            foreach (var result in results)
+            {
+                result(ref errors);
+            }
+        };
+    };
+
+    /// <summary>
+    /// Combines multiple validation rules where at least one must pass.
+    /// </summary>
+    /// <param name="funcs">The validation functions to combine.</param>
+    /// <returns>A combined validation rule that applies any of the specified rules.</returns>
+    public static FuncExpression Any(params FuncExpression[] funcs) => () =>
+    {
+        var results = new List<Func>();
+        foreach (var func in funcs)
+        {
+            if (func() is var fn && fn is { })
+            {
+                results.Add(fn);
+            }
+        }
+
+        if (results.Count == funcs.Length)
+        {
+            return (ref ValidationErrorBuilder errors) =>
+            {
+                foreach (var result in results)
+                {
+                    result(ref errors);
+                }
+            };
+        }
+
+        return null;
+    };
+
+    /// <summary>
+    /// Validates the default query parameters for an end user.
+    /// </summary>
+    /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+    /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+    /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> with validation errors if any.
+    /// </returns>
+    public static ValidationProblemInstance EnduserAddConnection(string party, string from, string to) => Validate(
+        QueryParameters.Party(party),
+        QueryParameters.PartyFrom(from),
+        QueryParameters.PartyTo(to),
+        QueryParameters.EnduserAddCombination(party, from, to)
+    );
+
+    /// <summary>
+    /// Validates the default query parameters for an end user.
+    /// </summary>
+    /// <param name="userUuid">The UUID of the user performing the action (current user).</param>
+    /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+    /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+    /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> with validation errors if any.
+    /// </returns>
+    public static ValidationProblemInstance EnduserReadConnection(string party, string from, string to) => Validate(
+        QueryParameters.Party(party),
+        QueryParameters.EnduserReadInputCombination(party, from, to),
+        Any(
+            QueryParameters.PartyFrom(from),
+            QueryParameters.PartyTo(to)
+        )
+    );
+
+    /// <summary>
+    /// Validates the default query parameters for an end user.
+    /// </summary>
+    /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+    /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+    /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> with validation errors if any.
+    /// </returns>
+    public static ValidationProblemInstance EnduserRemoveConnection(string party, string from, string to) => Validate(
+        QueryParameters.Party(party),
+        QueryParameters.PartyFrom(from),
+        QueryParameters.PartyTo(to),
+        QueryParameters.EnduserRemoveCombination(party, from, to)
+    );
+
+    /// <summary>
+    /// Provides validation rules for query parameters.
+    /// </summary>
+    public static class QueryParameters
+    {
+        private static IEnumerable<string> ParamKeywords { get; } = ["me", "all"];
+
+        private static string ParamKeywordsToString { get; } = "<me, all | blank, uuid>";
+
+        /// <summary>
+        /// Checks if any packages exist with given ID.
+        /// </summary>
+        /// <param name="packages">Lists of packages</param>
+        /// <param name="paramName">name of query parameter</param>
+        /// <returns></returns>
+        public static FuncExpression AnyPackages(IEnumerable<ConnectionPackage> packages, string paramName = "packageId") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.Any())
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.InvalidResource, $"QUERY/{paramName}", [new("Packages", $"No packages found.")]);
+        };
+
+        /// <summary>
+        /// Checks the list of packages all <see cref="Package.IsAssignable"/> is set to true.
+        /// </summary>
+        /// <param name="packages">list of packages</param>
+        /// <param name="paramName">name of the query parameter</param>
+        /// <returns></returns>
+        public static FuncExpression PackageIsAssignableByDefinition(IEnumerable<ExtConnectionPackage> packages, string paramName = "packageId") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.All(t => t.Package.IsAssignable))
+            {
+                return null;
+            }
+
+            var packagesNotAssignable = packages
+                .Where(p => p.Package.IsAssignable)
+                .Select(p => p.Id);
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/{paramName}", [new("Packages", $"{string.Join(",", packagesNotAssignable)} are not assignable.")]);
+        };
+
+        /// <summary>
+        /// Checks is packages can be asigned by checking <see cref="ConnectionPackage.CanAssign"/> is set to to true.
+        /// </summary>
+        /// <param name="packages">List of packages.</param>
+        /// <param name="paramName">name of the query parameter.</param>
+        /// <returns></returns>
+        public static FuncExpression PackageIsAssignableByUser(IEnumerable<ConnectionPackage> packages, string paramName = "packageId") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.All(t => t.CanAssign))
+            {
+                return null;
+            }
+
+            var packagesNotAssignableByUser = packages
+                .Where(p => p.CanAssign)
+                .Select(p => p.Id);
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/{paramName}", [new("Packages", $"Packages with IDs {string.Join(",", packagesNotAssignableByUser)} can't be assigned by user.")]);
+        };
+
+        /// <summary>
+        /// Used to check if package exists by check URN and resulkt of the DB lookup.
+        /// </summary>
+        /// <param name="packages">List of packags</param>
+        /// <param name="paramName">name of the query URN parameter.</param>
+        /// <returns></returns>
+        public static FuncExpression PackageUrnLookup(IEnumerable<Package> packages, string paramName = "packageUrn") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages?.Count() == 1)
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.AssignmentIsActiveInOneOrMoreDelegations, $"QUERY/{paramName}", [new("packages", string.Join(",", packages.Select(p => p.Id.ToString())))]);
+        };
+
+        /// <summary>
+        /// Checks if any packages are assigned. Used along with cascade delete
+        /// </summary>
+        /// <param name="packages">List of packages.</param>
+        /// <param name="paramName">name of the query parameter.</param>
+        public static FuncExpression HasPackagesAssigned(IEnumerable<AssignmentPackage> packages, string paramName = "cascade") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.Any())
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.AssignmentIsActiveInOneOrMoreDelegations, $"QUERY/{paramName}", [new("packages", $"following packages has active assignments [{string.Join(",", packages.Select(p => p.Id.ToString()))}].")]);
+            }
+
+            return null;
+        };
+
+        /// <summary>
+        /// Checks whether any packages are assigned. Used in conjunction with cascade delete.
+        /// </summary>
+        /// <param name="packages">List of role assignments.</param>
+        /// <param name="roleCode">The name of the role to verify.</param>
+        /// <param name="paramNameFrom">The name of the source query parameter.</param>
+        /// <param name="paramNameTo">The name of the target query parameter.</param>
+        public static FuncExpression VerifyAssignmentRoleExists(IEnumerable<Assignment> packages, string roleCode, string paramNameFrom = "from", string paramNameTo = "to") => () =>
+        {
+            if (packages is { } && packages.Any())
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+            {
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/{paramNameFrom}", [new("packages", $"No active role assignments of type '{roleCode}' were found from the value in query parameter '{paramNameFrom}' to the value in query parameter '{paramNameTo}'.")]);
+            };
+        };
+
+        /// <summary>
+        /// Checks if any delegations are given assigned. Used along with cascade delete
+        /// </summary>
+        /// <param name="delegations">List of delegations.</param>
+        /// <param name="paramName">name of the query parameter.</param>
+        public static FuncExpression HasDelegationsAssigned(IEnumerable<Delegation> delegations, string paramName = "cascade") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(delegations);
+            if (delegations.Any())
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.EntityNotExists, $"QUERY/{paramName}", [new("packages", string.Join(",", delegations.Select(p => p.Id.ToString())))]);
+            }
+
+            return null;
+        };
+
+        /// <summary>
+        /// Checks if party exists
+        /// </summary>
+        /// <returns></returns>
+        public static FuncExpression PartyExists(Entity party, string paramName = "party") => () =>
+        {
+            if (party is { })
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.EntityNotExists, $"QUERY/{paramName}", [new("party", $"Entity do not exists.")]);
+        };
+
+        public static FuncExpression PartyIsEntityType(ExtEntity party, string entityType, string paramName = "party") => () =>
+        {
+            if (party is { })
+            {
+                if (party.Type.Name.Equals(entityType, StringComparison.InvariantCultureIgnoreCase))
+                {
+                    return null;
+                }
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.EntityNotExists, $"QUERY/{paramName}", [new("party", $"Entity do not exists or entity is not of type '{entityType}'.")]);
+        };
+
+        /// <summary>
+        /// Validates combination of input parameters
+        /// </summary>
+        public static FuncExpression EnduserAddCombination(string party, string from, string to) => () =>
+        {
+            if (!Guid.TryParse(party, out var partyUuid) || partyUuid == Guid.Empty)
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/party", [new("party", $"Paramater is not a valid a UUID.")]);
+            }
+
+            if (!Guid.TryParse(from, out var fromUuid) || fromUuid == Guid.Empty)
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/party", [new("from", $"Parameter is not a valid UUID.")]);
+            }
+
+            if (!Guid.TryParse(to, out var toUuid) || toUuid == Guid.Empty)
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/party", [new("to", $"Parameter is not a valid UUID.")]);
+            }
+
+            if (partyUuid != fromUuid)
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                {
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/from", [new("from", "must match the 'party' UUID.")]);
+                };
+            }
+
+            return null;
+        };
+
+        /// <summary>
+        /// Validates that the acting party matches either the 'from' or 'to' party,
+        /// and ensures that none of the provided UUIDs are invalid or empty. If any UUID is 
+        /// set to "me", it will be replaced by the user's UUID.
+        /// </summary>
+        /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+        /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+        /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+        /// <returns>
+        /// A validation rule that adds an error if any UUID is invalid or empty, 
+        /// or if the party does not match either the 'from' or 'to' UUID.
+        /// </returns>
+        public static FuncExpression EnduserReadInputCombination(string party, string from, string to) => () =>
+        {
+            if (!Guid.TryParse(party, out var partyUuid))
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/party", [new("party", $"The 'party' parameter is not valid.")]);
+            }
+
+            if (!string.IsNullOrEmpty(from) && Guid.TryParse(from, out var fromUuid) && fromUuid == partyUuid)
+            {
+                return null;
+            }
+
+            if (!string.IsNullOrEmpty(to) && Guid.TryParse(to, out var toUuid) && toUuid == partyUuid)
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+            {
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/from", [new("from", "Either the 'from' UUID or the 'to' UUID must match the 'party' UUID. Neither matches the 'party' UUID.")]);
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/to", [new("to", "Either the 'to' UUID or the 'from' UUID must match the 'party' UUID. Neither matches the 'party' UUID.")]);
+            };
+        };
+
+        /// <summary>
+        /// Delete combination
+        /// </summary>
+        /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+        /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+        /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+        /// <returns></returns>
+        public static FuncExpression EnduserRemoveCombination(string party, string from, string to) => () =>
+        {
+            if (!Guid.TryParse(party, out var partyUuid))
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/party", [new("party", $"The 'party' parameter is not valid.")]);
+            }
+
+            if (!Guid.TryParse(from, out var fromUuid) || fromUuid == Guid.Empty)
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/from", [new("from", $"Parameter is not a valid UUID.")]);
+            }
+
+            if (!Guid.TryParse(to, out var toUuid) || toUuid == Guid.Empty)
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/to", [new("to", $"Parameter is not a valid UUID.")]);
+            }
+
+            if (partyUuid != fromUuid && partyUuid != toUuid)
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                {
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/from", [new("from", "Either the 'from' UUID or the 'to' UUID must match the 'party' UUID. Neither matches the 'party' UUID.")]);
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/to", [new("to", "Either the 'to' UUID or the 'from' UUID must match the 'party' UUID. Neither matches the 'party' UUID.")]);
+                };
+            }
+
+            return null;
+        };
+
+        /// <summary>
+        /// Validates that a party query parameter is either a valid UUID
+        /// or matches one of the predefined keywords (e.g., "me", "all") <see cref="ParamKeywords"/>.
+        /// </summary>
+        /// <param name="party">The value of the party query parameter to validate.</param>
+        /// <param name="paramName">The name of the query parameter (used for error reporting).</param>
+        /// <param name="values"></param>
+        /// <returns>
+        /// A validation rule that adds an error if the party value is not a valid UUID
+        /// and does not match any of the predefined keywords <see cref="ParamKeywords"/>.
+        /// </returns>
+        public static FuncExpression PartyIs(string party, string paramName = "party", params string[] values) => () =>
+        {
+            if (Guid.TryParse(party, out var _))
+            {
+                return null;
+            }
+
+            foreach (var value in values)
+            {
+                if (string.Equals(party, value, StringComparison.InvariantCultureIgnoreCase))
+                {
+                    return null;
+                }
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/{paramName}", [new("party", $"Party '{party}' must be a valid UUID or one of <{ParamKeywordsToString}>.")]);
+        };
+
+        /// <summary>
+        /// Validates that a party query parameter is either a valid UUID
+        /// or matches one of the predefined keywords (e.g., "me", "all") <see cref="ParamKeywords"/>.
+        /// </summary>
+        /// <param name="party">The value of the party query parameter to validate.</param>
+        /// <param name="paramName">The name of the query parameter (used for error reporting).</param>
+        /// <returns>
+        /// A validation rule that adds an error if the party value is not a valid UUID
+        /// and does not match any of the predefined keywords <see cref="ParamKeywords"/>.
+        /// </returns>
+        public static FuncExpression Party(string party, string paramName = "party") => () =>
+        {
+            if (Guid.TryParse(party, out var _))
+            {
+                return null;
+            }
+
+            foreach (var keyword in ParamKeywords)
+            {
+                if (string.Equals(party, keyword, StringComparison.InvariantCultureIgnoreCase))
+                {
+                    return null;
+                }
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/{paramName}", [new("party", $"Given party of combination of parties must be a valid UUID or one of {ParamKeywordsToString}.")]);
+        };
+
+        /// <summary>
+        /// Validates the 'from' party query parameter.
+        /// The value must be a valid UUID or one of the predefined keywords ("me", "all") <see cref="ParamKeywords"/>.
+        /// </summary>
+        /// <param name="party">The value of the 'from' query parameter to validate.</param>
+        /// <returns>A validation rule that adds an error if validation fails.</returns>
+        public static FuncExpression PartyFrom(string party) => Party(party, "from");
+
+        /// <summary>
+        /// Validates the 'to' party query parameter.
+        /// The value must be a valid UUID or one of the predefined keywords ("me", "all") <see cref="ParamKeywords"/>.
+        /// </summary>
+        /// <param name="party">The value of the 'to' query parameter to validate.</param>
+        /// <returns>A validation rule that adds an error if validation fails.</returns>
+        public static FuncExpression PartyTo(string party) => Party(party, "to");
+    }
+}
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Mappers/AssignmentExternalMapper.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Mappers/AssignmentExternalMapper.cs
deleted file mode 100644
index ed83b530d..000000000
--- a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Mappers/AssignmentExternalMapper.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using Altinn.AccessManagement.Api.Enduser.Models;
-using Altinn.AccessMgmt.Core.Models;
-
-namespace Altinn.AccessManagement.Api.Enduser.Mappers;
-
-/// <summary>
-/// Maps between <see cref="AssignmentExternal"/> and <see cref="Assignment"/>.
-/// </summary>
-public class AssignmentExternalMapper : IMapper<AssignmentExternal, Assignment>
-{
-    /// <inheritdoc/>
-    public AssignmentExternal Map(Assignment from)
-    {
-        return new()
-        {
-            Id = from.Id,
-            FromId = from.FromId,
-            ToId = from.ToId,
-            RoleId = from.RoleId,
-        };
-    }
-}
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Mappers/IMapper.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Mappers/IMapper.cs
deleted file mode 100644
index 37bd50ccb..000000000
--- a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Mappers/IMapper.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace Altinn.AccessManagement.Api.Enduser.Mappers;
-
-/// <summary>
-/// Defines a contract for mapping objects of type <typeparamref name="TFrom"/> to type <typeparamref name="TTo"/>.
-/// </summary>
-/// <typeparam name="TTo">The target type to map to.</typeparam>
-/// <typeparam name="TFrom">The source type to map from.</typeparam>
-public interface IMapper<out TTo, in TFrom>
-{
-    /// <summary>
-    /// Maps an instance of <typeparamref name="TFrom"/> to an instance of <typeparamref name="TTo"/>.
-    /// </summary>
-    /// <param name="from">The source object to map from.</param>
-    /// <returns>A mapped instance of <typeparamref name="TTo"/>.</returns>
-    TTo Map(TFrom from);
-}
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Models/AssignmentExternal.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Models/AssignmentExternal.cs
deleted file mode 100644
index 2709ccc42..000000000
--- a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Models/AssignmentExternal.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System.Text.Json.Serialization;
-
-namespace Altinn.AccessManagement.Api.Enduser.Models;
-
-/// <summary>
-/// Assignment Model
-/// </summary>
-public class AssignmentExternal
-{
-    /// <summary>
-    /// Identity
-    /// </summary>
-    [JsonPropertyName("Id")]
-    public Guid Id { get; init; }
-
-    /// <summary>
-    /// RoleId
-    /// </summary>
-    [JsonPropertyName("RoleId")]
-    public Guid RoleId { get; init; }
-
-    /// <summary>
-    /// FromId
-    /// </summary>
-    [JsonPropertyName("FromId")]
-    public Guid FromId { get; init; }
-
-    /// <summary>
-    /// ToId
-    /// </summary>
-    [JsonPropertyName("ToId")]
-    public Guid ToId { get; init; }
-}
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Models/ConnectionInput.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Models/ConnectionInput.cs
new file mode 100644
index 000000000..0bd0b5247
--- /dev/null
+++ b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Models/ConnectionInput.cs
@@ -0,0 +1,31 @@
+using Microsoft.AspNetCore.Mvc;
+using Swashbuckle.AspNetCore.Annotations;
+
+namespace Altinn.AccessManagement.Api.Enduser.Models;
+
+/// <summary>
+/// Input for connection controller.
+/// </summary>
+public class ConnectionInput
+{
+    /// <summary>
+    /// making request on behalf of.
+    /// </summary>
+    [FromQuery(Name = "party")]
+    [SwaggerSchema(Description = "party", Format = "<me, uuid>")]
+    public string Party { get; set; }
+
+    /// <summary>
+    /// from party
+    /// </summary>
+    [FromQuery(Name = "from")]
+    [SwaggerSchema(Description = "from", Format = "<me, all | blank, uuid>")]
+    public string From { get; set; }
+
+    /// <summary>
+    /// to party
+    /// </summary>
+    [FromQuery(Name = "to")]
+    [SwaggerSchema(Description = "to", Format = "<me, all | blank, uuid>")]
+    public string To { get; set; }
+}
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Models/PagingInput.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Models/PagingInput.cs
new file mode 100644
index 000000000..f388e97fe
--- /dev/null
+++ b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Models/PagingInput.cs
@@ -0,0 +1,45 @@
+using System.ComponentModel;
+using System.Text.Json;
+using Microsoft.AspNetCore.Mvc;
+using Swashbuckle.AspNetCore.Annotations;
+using Swashbuckle.AspNetCore.Filters;
+
+namespace Altinn.AccessManagement.Api.Enduser.Models;
+
+/// <summary>
+/// Page Inputs.
+/// </summary>
+public class PagingInput : IExamplesProvider<PagingInput>
+{
+    /// <summary>
+    /// Number of elements requested from page. 
+    /// </summary>
+    [FromHeader(Name = "X-Page-Size")]
+    [DefaultValue(100)]
+    [SwaggerSchema(Description = "Page Size", Format = "[0, 100]")]
+    public uint PageSize { get; set; } = 100;
+
+    /// <summary>
+    /// Number of paged skipped
+    /// </summary>
+    [DefaultValue(0)]
+    [FromHeader(Name = "X-Page-Number")]
+    [SwaggerSchema(Description = "Page Number", Format = "[0, ∞)")]
+    public uint PageNumber { get; set; } = 0;
+
+    /// <inheritdoc/>
+    public PagingInput GetExamples()
+    {
+        return new PagingInput
+        {
+            PageNumber = 2,
+            PageSize = 56,
+        };
+    }
+
+    internal string ToOpaqueToken() =>
+        Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(this));
+
+    static internal PagingInput CreateFromToken(string token) =>
+        JsonSerializer.Deserialize<PagingInput>(Convert.FromBase64String(token));
+}
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Services/ConnectionService.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Services/ConnectionService.cs
new file mode 100644
index 000000000..c0e60a8cb
--- /dev/null
+++ b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Services/ConnectionService.cs
@@ -0,0 +1,420 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using Altinn.AccessManagement.Core.Errors;
+using Altinn.AccessMgmt.Core.Models;
+using Altinn.AccessMgmt.Persistence.Core.Helpers;
+using Altinn.AccessMgmt.Persistence.Repositories.Contracts;
+using Altinn.AccessMgmt.Persistence.Services;
+using Altinn.Authorization.ProblemDetails;
+
+namespace Altinn.AccessManagement.Enduser.Services;
+
+/// <summary>
+/// Service for managing connections.
+/// </summary>
+public class ConnectionService(
+    IDbAudit dbAudit,
+    IConnectionRepository connectionRepository,
+    IConnectionPackageRepository connectionPackageRepository,
+    IPackageRepository packageRepository,
+    IRoleRepository roleRepository,
+    IAssignmentRepository assignmentRepository,
+    IDelegationRepository delegationRepository,
+    IAssignmentPackageRepository assignmentPackageRepository,
+    IEntityRepository entityRepository
+    ) : IEnduserConnectionService
+{
+    private IDbAudit DbAudit { get; } = dbAudit;
+
+    private IConnectionRepository ConnectionRepository { get; } = connectionRepository;
+
+    private IConnectionPackageRepository ConnectionPackageRepository { get; } = connectionPackageRepository;
+
+    private IPackageRepository PackageRepository { get; } = packageRepository;
+
+    private IRoleRepository RoleRepository { get; } = roleRepository;
+
+    private IAssignmentRepository AssignmentRepository { get; } = assignmentRepository;
+
+    private IDelegationRepository DelegationRepository { get; } = delegationRepository;
+
+    private IAssignmentPackageRepository AssignmentPackageRepository { get; } = assignmentPackageRepository;
+
+    private IEntityRepository EntityRepository { get; } = entityRepository;
+
+    /// <inheritdoc />
+    public async Task<Result<List<ExtConnection>>> Get(Guid? from = null, Guid? to = null, CancellationToken cancellationToken = default)
+    {
+        var filter = ConnectionRepository.CreateFilterBuilder();
+        static bool SetFrom(Guid? from, GenericFilterBuilder<Connection> filter)
+        {
+            if (from != null && from.HasValue && from.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.FromId, from);
+                return true;
+            }
+
+            filter.NotSet(t => t.FromId);
+            return false;
+        }
+
+        static bool SetTo(Guid? to, GenericFilterBuilder<Connection> filter)
+        {
+            if (to != null && to.HasValue && to.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.ToId, to);
+                return true;
+            }
+
+            filter.NotSet(t => t.ToId);
+            return false;
+        }
+
+        var isFromSet = SetFrom(from, filter);
+        var isToSet = SetTo(to, filter);
+        if (isFromSet || isToSet)
+        {
+            filter.NotSet(t => t.Id);
+            filter.NotSet(t => t.FacilitatorId);
+        }
+        else
+        {
+            Unreachable();
+        }
+
+        var result = await ConnectionRepository.GetExtended(filter, cancellationToken: cancellationToken);
+        return result?.ToList() ?? [];
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<Assignment>> AddAssignment(Guid from, Guid to, string roleCode, CancellationToken cancellationToken = default)
+    {
+        var dependencies = await GetAssignmentDependencies(from, to, roleCode, cancellationToken);
+        if (ValidateAssignmentData(dependencies.EntityFrom, dependencies.EntityTo, dependencies.Roles) is var problem && problem is { })
+        {
+            return problem;
+        }
+
+        var roleId = dependencies.Roles.First().Id;
+        var filter = AssignmentRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.ToId, to)
+            .Equal(t => t.RoleId, roleId);
+
+        var existingAssignment = await AssignmentRepository.Get(filter, cancellationToken: cancellationToken);
+        if (existingAssignment != null && existingAssignment.Any())
+        {
+            return existingAssignment.First();
+        }
+
+        var assignment = new Assignment()
+        {
+            FromId = from,
+            ToId = to,
+            RoleId = roleId,
+        };
+
+        var result = await AssignmentRepository.Create(assignment, DbAudit.Value, cancellationToken);
+        if (result == 0)
+        {
+            Unreachable();
+        }
+
+        return assignment;
+    }
+
+    /// <inheritdoc />
+    public async Task<ValidationProblemInstance> RemoveAssignment(Guid from, Guid to, string roleCode, bool cascade, CancellationToken cancellationToken = default)
+    {
+        var dependencies = await GetAssignmentDependencies(from, to, roleCode, cancellationToken);
+        if (ValidateAssignmentData(dependencies.EntityFrom, dependencies.EntityTo, dependencies.Roles) is var problem && problem is { })
+        {
+            return problem;
+        }
+
+        var roleId = dependencies.Roles.First().Id;
+        var filter = AssignmentRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.ToId, to)
+            .Equal(t => t.RoleId, roleId);
+
+        var existingAssignments = await AssignmentRepository.Get(filter, cancellationToken: cancellationToken);
+        if (existingAssignments != null && !existingAssignments.Any())
+        {
+            return null;
+        }
+
+        var existingAssignment = existingAssignments.First();
+
+        if (!cascade)
+        {
+            var packages = await AssignmentPackageRepository.Get(f => f.AssignmentId, existingAssignment.Id, cancellationToken: cancellationToken);
+            var delegationsFrom = await DelegationRepository.Get(f => f.FromId, existingAssignment.Id, cancellationToken: cancellationToken);
+            var delegationsTo = await DelegationRepository.Get(f => f.ToId, existingAssignment.Id, cancellationToken: cancellationToken);
+
+            problem = ValidationRules.Validate(
+                ValidationRules.QueryParameters.HasPackagesAssigned(packages),
+                ValidationRules.QueryParameters.HasDelegationsAssigned(delegationsFrom),
+                ValidationRules.QueryParameters.HasDelegationsAssigned(delegationsTo)
+            );
+            if (problem is { })
+            {
+                return problem;
+            }
+        }
+
+        var result = await AssignmentRepository.Delete(existingAssignment.Id, DbAudit.Value, cancellationToken: cancellationToken);
+        if (result == 0)
+        {
+            Unreachable();
+        }
+
+        return null;
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<List<ConnectionPackage>>> GetPackages(Guid? from, Guid? to, CancellationToken cancellationToken = default)
+    {
+        var filter = ConnectionPackageRepository.CreateFilterBuilder();
+        static bool SetFrom(Guid? from, GenericFilterBuilder<ConnectionPackage> filter)
+        {
+            if (from != null && from.HasValue && from.Value != Guid.Empty)
+            {
+                filter.Equal(f => f.FromId, from);
+                return true;
+            }
+
+            filter.NotSet(f => f.FromId);
+            return false;
+        }
+
+        static bool SetTo(Guid? to, GenericFilterBuilder<ConnectionPackage> filter)
+        {
+            if (to != null && to.HasValue && to.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.ToId, to);
+                return true;
+            }
+
+            filter.NotSet(t => t.ToId);
+            return false;
+        }
+
+        var isFromSet = SetFrom(from, filter);
+        var isToSet = SetTo(to, filter);
+        if (isFromSet || isToSet)
+        {
+            filter.NotSet(t => t.Id);
+            filter.NotSet(t => t.RoleId);
+            filter.NotSet(t => t.FacilitatorId);
+        }
+        else
+        {
+            Unreachable();
+        }
+
+        var result = await ConnectionPackageRepository.Get(filter, cancellationToken: cancellationToken);
+        return result.ToList();
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<AssignmentPackage>> AddPackage(Guid from, Guid to, string roleCode, string packageUrn, CancellationToken cancellationToken = default)
+    {
+        var filter = PackageRepository.CreateFilterBuilder()
+            .Add(t => t.Urn, packageUrn, FilterComparer.EndsWith);
+
+        var packages = await PackageRepository.Get(filter, cancellationToken: cancellationToken);
+        var problem = ValidationRules.Validate(ValidationRules.QueryParameters.PackageUrnLookup(packages));
+        if (problem is { })
+        {
+            return problem;
+        }
+
+        var package = packages.First();
+        return await AddPackage(from, to, roleCode, package.Id, "packageUrn", cancellationToken);
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<AssignmentPackage>> AddPackage(Guid from, Guid to, string roleCode, Guid packageId, CancellationToken cancellationToken = default)
+    {
+        return await AddPackage(from, to, roleCode, packageId, "packageId", cancellationToken);
+    }
+
+    private async Task<Result<AssignmentPackage>> AddPackage(Guid from, Guid to, string roleCode, Guid packageId, string queryParamName, CancellationToken cancellationToken)
+    {
+        var dependencies = await GetAssignmentDependencies(from, to, roleCode, cancellationToken);
+        if (ValidateAssignmentData(dependencies.EntityFrom, dependencies.EntityTo, dependencies.Roles) is var problem && problem is { })
+        {
+            return problem;
+        }
+
+        var roleId = dependencies.Roles.First().Id;
+        var filter = AssignmentRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.ToId, to)
+            .Equal(t => t.RoleId, roleId);
+
+        var existingAssignments = await AssignmentRepository.Get(filter, cancellationToken: cancellationToken);
+        problem = ValidationRules.Validate(ValidationRules.QueryParameters.VerifyAssignmentRoleExists(existingAssignments, roleCode));
+        if (problem is { })
+        {
+            return problem;
+        }
+
+        var assignment = existingAssignments.First();
+
+        var userPackageFilter = ConnectionPackageRepository.CreateFilterBuilder()
+            .Equal(t => t.ToId, DbAudit.Value.ChangedBy)
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.PackageId, packageId);
+
+        var userPackags = await ConnectionPackageRepository.GetExtended(userPackageFilter, cancellationToken: cancellationToken);
+        var userpackage = userPackags.Where(p => p.PackageId == packageId)
+            .ToList();
+
+        problem = ValidationRules.Validate(
+            ValidationRules.QueryParameters.AnyPackages(userpackage, queryParamName),
+            ValidationRules.QueryParameters.PackageIsAssignableByUser(userpackage, queryParamName),
+            ValidationRules.QueryParameters.PackageIsAssignableByDefinition(userpackage, queryParamName)
+        );
+
+        if (problem is { })
+        {
+            return problem;
+        }
+
+        var assignmentPackageFilter = AssignmentPackageRepository
+            .CreateFilterBuilder()
+            .Equal(t => t.AssignmentId, assignment.Id)
+            .Equal(t => t.PackageId, packageId);
+
+        var existingPackageAssignment = await AssignmentPackageRepository.Get(assignmentPackageFilter, cancellationToken: cancellationToken);
+        if (existingPackageAssignment != null && existingPackageAssignment.Any())
+        {
+            return existingPackageAssignment.First();
+        }
+
+        var createResult = await AssignmentPackageRepository.Create(new AssignmentPackage() { AssignmentId = assignment.Id, PackageId = packageId }, DbAudit.Value, cancellationToken: cancellationToken);
+        if (createResult == 0)
+        {
+            Unreachable();
+        }
+
+        var createCheckResult = await AssignmentPackageRepository.Get(t => t.AssignmentId, assignment.Id, cancellationToken: cancellationToken);
+        if (createCheckResult == null || !existingPackageAssignment.Any())
+        {
+            Unreachable();
+        }
+
+        return createCheckResult.First();
+    }
+
+    /// <inheritdoc/>
+    public async Task<ValidationProblemInstance> RemovePackage(Guid fromId, Guid toId, string roleCode, Guid packageId, CancellationToken cancellationToken = default)
+    {
+        var roleResult = await RoleRepository.Get(t => t.Code, roleCode, cancellationToken: cancellationToken);
+        if (roleResult == null || !roleResult.Any())
+        {
+            return null;
+        }
+
+        var assignmentFilter = AssignmentRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, fromId)
+            .Equal(t => t.ToId, toId)
+            .Equal(t => t.RoleId, roleResult.First().Id);
+
+        var result = await AssignmentRepository.Get(assignmentFilter, cancellationToken: cancellationToken);
+        if (result == null || !result.Any())
+        {
+            return null;
+        }
+
+        var assignment = result.First();
+
+        var connectionPackageFilter = ConnectionPackageRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, fromId)
+            .Equal(t => t.ToId, toId)
+            .Equal(t => t.PackageId, packageId);
+
+        var userPackages = await ConnectionPackageRepository.GetExtended(connectionPackageFilter, cancellationToken: cancellationToken);
+
+        if (!userPackages.Any())
+        {
+            return null;
+        }
+
+        var packageFilter = AssignmentPackageRepository.CreateFilterBuilder()
+            .Equal(t => t.AssignmentId, assignment.Id)
+            .Equal(t => t.PackageId, packageId);
+
+        var checkResult = await AssignmentPackageRepository.Get(assignmentFilter, cancellationToken: cancellationToken);
+        if (checkResult == null && !checkResult.Any())
+        {
+            return null;
+        }
+
+        var deleteResult = await AssignmentPackageRepository.DeleteCross(assignment.Id, packageId, DbAudit.Value, cancellationToken: cancellationToken);
+        if (deleteResult == 0)
+        {
+            return null;
+        }
+
+        return null;
+    }
+
+    private ValidationProblemInstance? ValidateAssignmentData(ExtEntity entityFrom, ExtEntity entityTo, IEnumerable<Role> roles)
+    {
+        var problem = ValidationRules.Validate(
+            ValidationRules.QueryParameters.PartyExists(entityFrom, "from"),
+            ValidationRules.QueryParameters.PartyExists(entityTo, "to"),
+            ValidationRules.QueryParameters.PartyIsEntityType(entityFrom, "Organisasjon", "from"),
+            ValidationRules.QueryParameters.PartyIsEntityType(entityTo, "Organisasjon", "to")
+        );
+
+        if (problem is { })
+        {
+            return problem;
+        }
+
+        if (roles is null || !roles.Any())
+        {
+            Unreachable();
+        }
+
+        return null;
+    }
+
+    private async Task<(ExtEntity EntityFrom, ExtEntity EntityTo, IEnumerable<Role> Roles)> GetAssignmentDependencies(Guid from, Guid to, string roleCode, CancellationToken cancellationToken)
+    {
+        var entityFromTask = EntityRepository.GetExtended(from, cancellationToken: cancellationToken);
+        var entityToTask = EntityRepository.GetExtended(to, cancellationToken: cancellationToken);
+        var roleTask = RoleRepository.Get(t => t.Code, roleCode, cancellationToken: cancellationToken);
+        return (await entityFromTask, await entityToTask, await roleTask);
+    }
+
+    [DoesNotReturn]
+    private void Unreachable()
+    {
+        throw new UnreachableException();
+    }
+}
+
+/// <summary>
+/// Interface for managing connections.
+/// </summary>
+public interface IEnduserConnectionService
+{
+    Task<Result<List<ExtConnection>>> Get(Guid? from = null, Guid? to = null, CancellationToken cancellationToken = default);
+
+    Task<Result<Assignment>> AddAssignment(Guid from, Guid to, string roleCode, CancellationToken cancellationToken = default);
+
+    Task<ValidationProblemInstance> RemoveAssignment(Guid from, Guid to, string roleCode, bool cascade, CancellationToken cancellationToken = default);
+
+    Task<Result<List<ConnectionPackage>>> GetPackages(Guid? from, Guid? to, CancellationToken cancellationToken = default);
+
+    Task<Result<AssignmentPackage>> AddPackage(Guid fromId, Guid toId, string rolecode, Guid packageId, CancellationToken cancellationToken = default);
+
+    Task<Result<AssignmentPackage>> AddPackage(Guid fromId, Guid toId, string rolecode, string packageUrn, CancellationToken cancellationToken = default);
+
+    Task<ValidationProblemInstance> RemovePackage(Guid fromId, Guid toId, string roleCode, Guid packageId, CancellationToken cancellationToken = default);
+}
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Core/Errors/ValidationErrors.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Core/Errors/ValidationErrors.cs
index 857cb5046..fafe67a35 100644
--- a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Core/Errors/ValidationErrors.cs
+++ b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Core/Errors/ValidationErrors.cs
@@ -50,26 +50,14 @@ private static readonly ValidationErrorDescriptorFactory _factory
     /// <summary>
     /// Missing party.
     /// </summary>
-    public static ValidationErrorDescriptor MissingPartyInDb { get; }
-        = _factory.Create(6, $"Missing party.");
+    public static ValidationErrorDescriptor EntityNotExists { get; }
+        = _factory.Create(6, $"Entity do not exists.");
 
     /// <summary>
     /// Invalid party type.
     /// </summary>
-    public static ValidationErrorDescriptor InvalidPartyType { get; }
-        = _factory.Create(7, $"Invalid party type.");
-
-    /// <summary>
-    /// Assignment already exists.
-    /// </summary>
-    public static ValidationErrorDescriptor AssignmentAlreadyExists { get; }
-        = _factory.Create(8, $"Assignment already exists.");
-
-    /// <summary>
-    /// Assignment do not exists.
-    /// </summary>
-    public static ValidationErrorDescriptor AssignmentDoNotExists { get; }
-        = _factory.Create(9, $"Assignment do not exist.");
+    public static ValidationErrorDescriptor InvalidQueryParameter { get; }
+        = _factory.Create(7, $"One or more query parameters are invalid.");
 
     /// <summary>
     /// Assignment is active in one or more delegations.
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Core/Extensions/HttpContextAccessorExtensions.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Core/Extensions/HttpContextAccessorExtensions.cs
deleted file mode 100644
index 070e3c4b9..000000000
--- a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Core/Extensions/HttpContextAccessorExtensions.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using Altinn.AccessManagement.Core.Constants;
-using Altinn.AccessManagement.Core.Filters;
-using Microsoft.AspNetCore.Http;
-
-namespace Altinn.AccessManagement.Core.Extensions;
-
-/// <summary>
-/// Provides extension methods for accessing and manipulating data within the HTTP context.
-/// </summary>
-/// <remarks>
-/// This class contains extension methods that simplify accessing specific data, like the party UUID, 
-/// from the HTTP context in a way that can be easily used throughout the application.
-/// </remarks>
-public static class HttpContextAccessorExtensions
-{
-    /// <summary>
-    /// Retrieves the party UUID from the HTTP context items.
-    /// </summary>
-    /// <param name="accessor">The HTTP context accessor used to access the current request's context.</param>
-    /// <returns>The party UUID if found.</returns>
-    /// <exception cref="InvalidOperationException">
-    /// Thrown if the UUID is not present in the context. Ensure that the 
-    /// <see cref="AuthorizePartyUuidClaimFilter"/> is enabled for the controller action.
-    /// </exception>
-    public static Guid GetPartyUuid(this IHttpContextAccessor accessor)
-    {
-        var claim = accessor.HttpContext.User?.Claims?
-            .FirstOrDefault(c => c.Type.Equals(AltinnCoreClaimTypes.PartyUuid, StringComparison.OrdinalIgnoreCase));
-
-        if (claim != null && Guid.TryParse(claim.Value, out var result))
-        {
-            return result;
-        }
-
-        throw new InvalidOperationException($"Failed to retrieve UUID. Is the '{nameof(AuthorizePartyUuidClaimFilter)}' ServiceFilter enabled for this action?");
-    }
-}
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Core/Filters/AuthorizePartyUuidClaimFilter.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Core/Filters/AuthorizePartyUuidClaimFilter.cs
deleted file mode 100644
index b63f35995..000000000
--- a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Core/Filters/AuthorizePartyUuidClaimFilter.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using Altinn.AccessManagement.Core.Constants;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.Filters;
-using Microsoft.Extensions.Logging;
-
-namespace Altinn.AccessManagement.Core.Filters;
-
-/// <summary>
-/// Verifies that user uuid claim exists in token and adds the value to http context items bag.
-/// If claim is not present it will return cancel pipeline and return 401 unauthorized.
-/// </summary>
-public partial class AuthorizePartyUuidClaimFilter(ILogger<AuthorizePartyUuidClaimFilter> logger) : IAuthorizationFilter
-{
-    private ILogger<AuthorizePartyUuidClaimFilter> Logger { get; } = logger;
-
-    /// <inheritdoc/>
-    public void OnAuthorization(AuthorizationFilterContext context)
-    {
-        if (context.HttpContext.User?.Identity?.IsAuthenticated != true)
-        {
-            Log.ThereIsNoAuthorizedUser(Logger);
-            context.Result = new UnauthorizedResult();
-            return;
-        }
-
-        var claim = context.HttpContext.User?.Claims?
-            .FirstOrDefault(c => c.Type.Equals(AltinnCoreClaimTypes.PartyUuid, StringComparison.OrdinalIgnoreCase));
-
-        if (claim != null && Guid.TryParse(claim.Value, out _))
-        {
-            return;
-        }
-
-        Log.MissingClaimInToken(Logger);
-        context.Result = new ForbidResult();
-    }
-
-    static partial class Log
-    {
-        [LoggerMessage(EventId = 1, Level = LogLevel.Warning, Message = $"User is not authenticated.")]
-        internal static partial void ThereIsNoAuthorizedUser(ILogger logger);
-
-        [LoggerMessage(EventId = 0, Level = LogLevel.Warning, Message = $"User is authorized, but the claim '{AltinnCoreClaimTypes.PartyUuid}' is somehow missing from the token.")]
-        internal static partial void MissingClaimInToken(ILogger logger);
-    }
-}
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement/AccessManagementHost.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement/AccessManagementHost.cs
index 08f8d22a3..cc3b0d7f2 100644
--- a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement/AccessManagementHost.cs
+++ b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement/AccessManagementHost.cs
@@ -4,7 +4,6 @@
 using Altinn.AccessManagement.Core.Configuration;
 using Altinn.AccessManagement.Core.Constants;
 using Altinn.AccessManagement.Core.Extensions;
-using Altinn.AccessManagement.Core.Filters;
 using Altinn.AccessManagement.Health;
 using Altinn.AccessManagement.HostedServices.Contracts;
 using Altinn.AccessManagement.HostedServices.Services;
@@ -189,6 +188,7 @@ private static void ConfigureOpenAPI(this WebApplicationBuilder builder)
                 Type = SecuritySchemeType.ApiKey
             });
             options.OperationFilter<SecurityRequirementsOperationFilter>();
+            options.EnableAnnotations();
 
             var originalIdSelector = options.SchemaGeneratorOptions.SchemaIdSelector;
             options.SchemaGeneratorOptions.SchemaIdSelector = (Type t) =>
@@ -281,7 +281,6 @@ private static void ConfigureAuthorization(this WebApplicationBuilder builder)
         builder.Services.AddScoped<IAuthorizationHandler, ResourceAccessHandler>();
         builder.Services.AddScoped<IAuthorizationHandler, EndUserResourceAccessHandler>();
         builder.Services.AddScoped<IAuthorizationHandler, ScopeAccessHandler>();
-        builder.Services.AddScoped<AuthorizePartyUuidClaimFilter>();
     }
 
     private static void ConfigurePostgreSqlConfiguration(this WebApplicationBuilder builder)
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement/Program.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement/Program.cs
index 46a085aa0..505cd241a 100644
--- a/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement/Program.cs
+++ b/src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement/Program.cs
@@ -28,6 +28,7 @@
 
 app.UseAuthentication();
 app.UseAuthorization();
+app.UseDbAudit();
 
 app.MapControllers();
 app.MapDefaultAltinnEndpoints();
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence.Core/Helpers/GenericFilterBuilder.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence.Core/Helpers/GenericFilterBuilder.cs
index e7877246d..3926de723 100644
--- a/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence.Core/Helpers/GenericFilterBuilder.cs
+++ b/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence.Core/Helpers/GenericFilterBuilder.cs
@@ -9,7 +9,12 @@ namespace Altinn.AccessMgmt.Persistence.Core.Helpers;
 /// <typeparam name="T">The entity type for which the filters are being built.</typeparam>
 public class GenericFilterBuilder<T> : IEnumerable<GenericFilter>
 {
-    private readonly List<GenericFilter> _filters = new();
+    private readonly List<GenericFilter> _filters = [];
+
+    /// <summary>
+    /// Returns true if any filters are specified in list.
+    /// </summary>
+    public bool Empty => _filters.Count == 0;
 
     /// <summary>
     /// Adds a filter condition to the builder using the specified property expression, value, and comparer.
@@ -44,7 +49,7 @@ public GenericFilterBuilder<T> Equal<TProperty>(Expression<Func<T, TProperty>> p
     /// <typeparam name="TProperty">The type of the property.</typeparam>
     /// <param name="property">An expression selecting the property to filter on.</param>
     /// <returns>The current instance of <see cref="GenericFilterBuilder{T}"/>, enabling a fluent API.</returns>
-    public GenericFilterBuilder<T> IsNull<TProperty>(Expression<Func<T, TProperty>> property)
+    public GenericFilterBuilder<T> NotSet<TProperty>(Expression<Func<T, TProperty>> property)
     {
         var propertyInfo = ExtractPropertyInfo(property);
         _filters.Add(new GenericFilter(propertyInfo.Name, null, FilterComparer.Equals));
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Data/AuditDefaults.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Data/AuditDefaults.cs
index a44b5a5a7..f4374781d 100644
--- a/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Data/AuditDefaults.cs
+++ b/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Data/AuditDefaults.cs
@@ -18,5 +18,10 @@ public static class AuditDefaults
     /// <summary>
     /// EnduserApi
     /// </summary>
-    public static readonly Guid EnduserApi = Guid.Parse("ED771364-42A8-4934-801E-B482ED20EC3E");
+    public static readonly Guid EnduserApi = Guid.Parse(EnduserApiStr);
+
+    /// <summary>
+    /// EnduserApiStr
+    /// </summary>
+    public const string EnduserApiStr = "ED771364-42A8-4934-801E-B482ED20EC3E";
 }
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Extensions/DbAccessHostExtensions.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Extensions/DbAccessHostExtensions.cs
index c82f11628..dbc7534ec 100644
--- a/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Extensions/DbAccessHostExtensions.cs
+++ b/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Extensions/DbAccessHostExtensions.cs
@@ -11,6 +11,7 @@
 using Altinn.AccessMgmt.Persistence.Services;
 using Altinn.AccessMgmt.Persistence.Services.Contracts;
 using Altinn.Authorization.Host.Startup;
+using Microsoft.AspNetCore.Builder;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
@@ -27,6 +28,15 @@ public static partial class DbAccessHostExtensions
     /// </summary>
     private static ILogger Logger { get; } = StartupLoggerFactory.Create(nameof(DbAccessHostExtensions));
 
+    /// <summary>
+    /// Adds Db Audit Middleware
+    /// </summary>
+    public static IApplicationBuilder UseDbAudit(this IApplicationBuilder builder)
+    {
+        builder.UseMiddleware<AuditMiddleware>();
+        return builder;
+    }
+
     /// <summary>
     /// Adds database access services to the application builder.
     /// </summary>
@@ -56,6 +66,10 @@ public static IHostApplicationBuilder AddAccessMgmtDb(this IHostApplicationBuild
             }
         }
 
+        builder.Services.AddSingleton<AuditMiddleware>();
+        builder.Services.AddSingleton<IDbAudit, AuditFactory>();
+        builder.Services.AddSingleton<IDbAuditService, AuditFactory>();
+
         builder.Services.AddSingleton<DbDefinitionRegistry>();
         builder.Services.AddSingleton(typeof(ISearchCache<>), typeof(SearchCache<>));
 
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Services/AssignmentService.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Services/AssignmentService.cs
index b9114bd2c..8f629f5c5 100644
--- a/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Services/AssignmentService.cs
+++ b/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Services/AssignmentService.cs
@@ -121,7 +121,7 @@ await assignmentPackageRepository.Create(
                 AssignmentId = assignmentId,
                 PackageId = packageId
             },
-            options: options, 
+            options: options,
             cancellationToken: cancellationToken
         );
 
@@ -324,7 +324,7 @@ private static void ValidatePartyIsNotNull(Guid id, ExtEntity entity, ref Valida
     {
         if (entity is null)
         {
-            errors.Add(ValidationErrors.MissingPartyInDb, param, [new("partyId", id.ToString())]);
+            errors.Add(ValidationErrors.EntityNotExists, param, [new("partyId", id.ToString())]);
         }
     }
 
@@ -332,7 +332,7 @@ private static void ValidatePartyIsOrg(ExtEntity entity, ref ValidationErrorBuil
     {
         if (entity is not null && !entity.Type.Name.Equals("Organisasjon", StringComparison.InvariantCultureIgnoreCase))
         {
-            errors.Add(ValidationErrors.InvalidPartyType, param, [new("partyId", $"expected party of type 'Organisasjon' got '{entity.Type.Name}'.")]);
+            errors.Add(ValidationErrors.InvalidQueryParameter, param, [new("partyId", $"expected party of type 'Organisasjon' got '{entity.Type.Name}'.")]);
         }
     }
 
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Services/AuditService.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Services/AuditService.cs
new file mode 100644
index 000000000..e6b0b1e2d
--- /dev/null
+++ b/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Services/AuditService.cs
@@ -0,0 +1,100 @@
+using Altinn.AccessMgmt.Persistence.Core.Models;
+using Microsoft.AspNetCore.Http;
+
+namespace Altinn.AccessMgmt.Persistence.Services;
+
+/// <summary>
+/// Middleware that sets Audit state
+/// </summary>
+internal class AuditMiddleware(IDbAuditService auditService) : IMiddleware
+{
+    private IDbAuditService AuditService { get; } = auditService;
+
+    /// <summary>
+    /// Middleware that creates change request object
+    /// </summary>
+    /// <param name="context"><see cref="HttpContext"/></param>
+    /// <param name="next"><see cref="RequestDelegate"/></param>
+    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
+    {
+        if (context.GetEndpoint() is var endpoint && endpoint != null)
+        {
+            if (endpoint.Metadata.GetMetadata<DbAuditAttribute>() is var attr && attr != null)
+            {
+                var claim = context.User?.Claims?
+                    .FirstOrDefault(c => c.Type.Equals(attr.Claim, StringComparison.OrdinalIgnoreCase));
+
+                if (claim != null && Guid.TryParse(claim.Value, out var uuid))
+                {
+                    AuditService.Set(new()
+                    {
+                        ChangedBy = uuid,
+                        ChangedBySystem = Guid.Parse(attr.System),
+                        ChangeOperationId = context.TraceIdentifier,
+                    });
+                }
+            }
+        }
+
+        await next(context);
+    }
+}
+
+/// <summary>
+/// Attribute to decorate actions
+/// </summary>
+[AttributeUsage(AttributeTargets.Method)]
+public class DbAuditAttribute : Attribute
+{
+    /// <summary>
+    /// Token claim that gets set to ChangedBy. Claim must be UUID.
+    /// </summary>
+    public string Claim { get; set; }
+
+    /// <summary>
+    /// Which system that initiates the request.
+    /// </summary>
+    public string System { get; set; }
+}
+
+/// <inheritdoc/>
+internal class AuditFactory(IHttpContextAccessor accessor) : IDbAudit, IDbAuditService
+{
+    private IHttpContextAccessor Accessor { get; } = accessor;
+
+    /// <summary>
+    /// Gets the <see cref="ChangeRequestOptions"/> 
+    /// </summary>
+    public ChangeRequestOptions Value =>
+        Accessor.HttpContext.Items.TryGetValue(nameof(AuditFactory), out var result) ?
+            result as ChangeRequestOptions :
+            throw new InvalidOperationException($"Failed to retrieve {nameof(ChangeRequestOptions)} from HttpContext. Is action decorated with attribute {nameof(DbAuditAttribute)}?");
+
+    /// <summary>
+    /// Sets the <see cref="ChangeRequestOptions"/> 
+    /// </summary>
+    public void Set(ChangeRequestOptions options) =>
+        Accessor.HttpContext.Items.TryAdd(nameof(AuditFactory), options);
+}
+
+/// <summary>
+/// Factory for getting <see cref="ChangeRequestOptions"/> 
+/// </summary>
+public interface IDbAudit
+{
+    /// <summary>
+    /// Value
+    /// </summary>
+    public ChangeRequestOptions Value { get; }
+}
+
+/// <summary>
+/// Sets the Audit service
+/// </summary>
+internal interface IDbAuditService
+{
+    /// <summary>
+    /// Sets the ChangeRequestOptions 
+    /// </summary>
+    void Set(ChangeRequestOptions options);
+}
diff --git a/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Services/ConnectionService.cs b/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Services/ConnectionService.cs
index 66cfae64f..0c2c6f626 100644
--- a/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Services/ConnectionService.cs
+++ b/src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Services/ConnectionService.cs
@@ -27,9 +27,9 @@ public async Task<ExtConnection> Get(Guid Id, CancellationToken cancellationToke
     {
         var filter = connectionRepository.CreateFilterBuilder();
         filter.Equal(t => t.Id, Id);
-        filter.IsNull(t => t.FromId);
-        filter.IsNull(t => t.ToId);
-        filter.IsNull(t => t.FacilitatorId);
+        filter.NotSet(t => t.FromId);
+        filter.NotSet(t => t.ToId);
+        filter.NotSet(t => t.FacilitatorId);
         var res = await connectionRepository.GetExtended(filter, cancellationToken: cancellationToken);
         return res.FirstOrDefault();
     }
@@ -45,7 +45,7 @@ public async Task<IEnumerable<ExtConnection>> Get(Guid? fromId = null, Guid? toI
         }
         else
         {
-            filter.IsNull(t => t.FromId);
+            filter.NotSet(t => t.FromId);
         }
 
         if (toId.HasValue)
@@ -54,7 +54,7 @@ public async Task<IEnumerable<ExtConnection>> Get(Guid? fromId = null, Guid? toI
         }
         else
         {
-            filter.IsNull(t => t.ToId);
+            filter.NotSet(t => t.ToId);
         }
 
         if (facilitatorId.HasValue)
@@ -63,7 +63,7 @@ public async Task<IEnumerable<ExtConnection>> Get(Guid? fromId = null, Guid? toI
         }
         else
         {
-            filter.IsNull(t => t.FacilitatorId);
+            filter.NotSet(t => t.FacilitatorId);
         }
 
         if (!filter.Any())
@@ -71,7 +71,7 @@ public async Task<IEnumerable<ExtConnection>> Get(Guid? fromId = null, Guid? toI
             throw new ArgumentException("You need to define a filter");
         }
 
-        filter.IsNull(t => t.Id);
+        filter.NotSet(t => t.Id);
         return await connectionRepository.GetExtended(filter, cancellationToken: cancellationToken);
     }
 
@@ -150,13 +150,13 @@ public async Task<bool> AddPackage(Guid connectionId, Guid packageId, ChangeRequ
 
         var assignment = assignmentRepository.Get(connectionId, cancellationToken: cancellationToken);
         var delegation = delegationRepository.Get(connectionId, cancellationToken: cancellationToken);
-        
+
         if (assignment != null)
         {
             var res = await GetOrCreateAssignmentPackage(connectionId, packageId, options, cancellationToken: cancellationToken);
             return true;
         }
-        
+
         if (delegation != null)
         {
             var res = await GetOrCreateDelegationPackage(connectionId, packageId, options, cancellationToken: cancellationToken);
@@ -188,7 +188,7 @@ public async Task<bool> RemovePackage(Guid connectionId, Guid packageId, ChangeR
 
         return false;
     }
-    
+
     private async Task<IEnumerable<ConnectionPackage>> GetConnectionPackages(Guid? fromId, Guid? toId, CancellationToken cancellationToken = default)
     {
         /* Get packages assigned to entity from a specific entity, to see if entity can assign it to another entity */
@@ -207,7 +207,7 @@ Get all connectionpackages
         }
         else
         {
-            filter.IsNull(t => fromId);
+            filter.NotSet(t => fromId);
         }
 
         if (toId.HasValue)
@@ -216,7 +216,7 @@ Get all connectionpackages
         }
         else
         {
-            filter.IsNull(t => t.ToId);
+            filter.NotSet(t => t.ToId);
         }
 
         return (await connectionPackageRepository.Get(filter, cancellationToken: cancellationToken)).Data;


Reviews:
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T10:46:07Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T10:46:49Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T10:54:46Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T10:57:32Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T10:59:29Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T11:00:47Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T11:05:49Z
user: login: andreasisnes
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T11:14:13Z
user: login: andreasisnes
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T11:16:44Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T11:39:35Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T11:47:12Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T11:49:18Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T12:00:38Z
user: login: andreasisnes
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T12:06:16Z
user: login: andreasisnes
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T12:06:44Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T09:22:11Z
user: login: andreasisnes
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T09:45:58Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR refactors audit and connection services by updating filter methods, renaming authorization filters, and introducing new middleware and endpoints to improve error handling and audit logging. Key changes include:
- Replacing IsNull with NotSet in filter builders to align with updated patterns.
- Adding new AuditMiddleware and related service registrations and renaming AuthorizePartyUuidClaimFilter to SetEnduserAudit.
- Refactoring ConnectionController endpoints and updating validation error messages.

### Reviewed Changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 4 comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| ConnectionService.cs | Updated filter method calls from IsNull to NotSet. |
| AuditService.cs | Introduced new audit middleware and attribute usage. |
| AssignmentService.cs | Modified error codes in validation methods. |
| DbAccessHostExtensions.cs | Duplicate service registrations for audit services observed. |
| AuthorizePartyUuidClaimFilter.cs | Renamed to SetEnduserAudit with changes in unauthorized handling. |
| HttpContextAccessorExtensions.cs | Contains unreachable code due to misplaced return statement. |
| ValidationErrors.cs | Updated validation error messages with a potential grammatical error. |
| ConnectionController.cs | Refactored endpoints with missing return statements in error handling blocks. |
| Other files | Adjusted DI registrations and endpoint routes for enduser API. |
</details>


<details>
<summary>Files not reviewed (1)</summary>

* **src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Altinn.AccessManagement.Api.Enduser.csproj**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Core/Extensions/HttpContextAccessorExtensions.cs:35**
* Unreachable code detected: the throw statement after the return is never executed. Remove or reposition the throw statement to ensure proper error handling.
```
throw new InvalidOperationException("Failed to retrieve UUID. Is the 'SetEnduserAudit' ServiceFilter enabled for this action?");
```
**src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Core/Errors/ValidationErrors.cs:53**
* [nitpick] The error message contains a grammatical error; consider changing 'Entity do not exists.' to 'Entity does not exist.'
```
public static readonly ValidationErrorDescriptor EntityNotExists = _factory.Create(6, "Entity do not exists.");
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T09:53:42Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T09:57:49Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T10:14:11Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T10:14:42Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T10:18:04Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T10:22:01Z
user: login: andreasisnes
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T10:31:50Z
user: login: andreasisnes
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T10:31:58Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T10:39:48Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T10:56:20Z
user: login: andreasisnes
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T11:08:38Z
user: login: andreasisnes
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T11:08:46Z
user: login: andreasisnes
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T11:27:47Z
user: login: andreasisnes
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T11:27:59Z
user: login: andreasisnes
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T12:07:23Z
user: login: jonkjetiloye
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T14:32:52Z

Review comments:
diff_hunk: @@ -22,281 +23,209 @@ namespace Altinn.AccessManagement.Api.Enduser.Controllers;
 [Route("accessmanagement/api/v1/enduser/connections")]
 [FeatureGate(AccessManagementEnduserFeatureFlags.ControllerConnections)]
 [Authorize(Policy = AuthzConstants.SCOPE_PORTAL_ENDUSER)]
-public class ConnectionController(IHttpContextAccessor accessor, IConnectionService connectionService, IAssignmentService assignmentService, IEntityRepository entityRepository) : ControllerBase
+public class ConnectionController(IHttpContextAccessor accessor, IEnduserConnectionService connectionService, IDbAudit audit) : ControllerBase
 {
     private IHttpContextAccessor Accessor { get; } = accessor;
 
+    private IEnduserConnectionService ConnectionService { get; } = connectionService;
+
+    private IDbAudit Audit { get; } = audit;
+
     /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
+    /// Get connections between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpGet]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<AssignmentExternal>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetConnections([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
+        var userUuid = Audit.Value;
+        if (ValidationRules.EnduserGetConnection(userUuid.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
+        var result = await ConnectionService.Get(from: connection.From.ConvertToUuid(userUuid.ChangedBy), to: connection.To.ConvertToUuid(userUuid.ChangedBy), cancellationToken: cancellationToken);
+        if (result.IsProblem)
         {
-            // Party must match From or To
-            return BadRequest();
+            return result.Problem.ToActionResult();
         }
 
-        var partyUuid = Accessor.GetPartyUuid();
-        var audit = new ChangeRequestOptions()
-        {
-            ChangedBy = partyUuid,
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from.HasValue && to.HasValue)
-        {
-            return Ok(await connectionService.Get(fromId: from.Value, toId: to.Value, facilitatorId: null, cancellationToken: cancellationToken));
-        }
+        return Ok(PaginatedResult.Create(result.Value, null));
+    }
 
-        if (from.HasValue)
+    /// <summary>
+    /// Get connections between the authenticated user's selected party and the specified target party.
+    /// </summary>
+    [HttpGet("{id}")]
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
user: login: jonkjetiloye
body: Ikke ett API vi har noen plan for å bruke i noen kontekst p.t. så litt usikker på hvorfor/om vi i det hele tatt bør inkludere dette.
created_at: 2025-05-07T10:46:06Z
author_association: MEMBER
diff_hunk: @@ -22,281 +23,209 @@ namespace Altinn.AccessManagement.Api.Enduser.Controllers;
 [Route("accessmanagement/api/v1/enduser/connections")]
 [FeatureGate(AccessManagementEnduserFeatureFlags.ControllerConnections)]
 [Authorize(Policy = AuthzConstants.SCOPE_PORTAL_ENDUSER)]
-public class ConnectionController(IHttpContextAccessor accessor, IConnectionService connectionService, IAssignmentService assignmentService, IEntityRepository entityRepository) : ControllerBase
+public class ConnectionController(IHttpContextAccessor accessor, IEnduserConnectionService connectionService, IDbAudit audit) : ControllerBase
 {
     private IHttpContextAccessor Accessor { get; } = accessor;
 
+    private IEnduserConnectionService ConnectionService { get; } = connectionService;
+
+    private IDbAudit Audit { get; } = audit;
+
     /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
+    /// Get connections between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpGet]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<AssignmentExternal>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetConnections([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
+        var userUuid = Audit.Value;
+        if (ValidationRules.EnduserGetConnection(userUuid.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
+        var result = await ConnectionService.Get(from: connection.From.ConvertToUuid(userUuid.ChangedBy), to: connection.To.ConvertToUuid(userUuid.ChangedBy), cancellationToken: cancellationToken);
+        if (result.IsProblem)
         {
-            // Party must match From or To
-            return BadRequest();
+            return result.Problem.ToActionResult();
         }
 
-        var partyUuid = Accessor.GetPartyUuid();
-        var audit = new ChangeRequestOptions()
-        {
-            ChangedBy = partyUuid,
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from.HasValue && to.HasValue)
-        {
-            return Ok(await connectionService.Get(fromId: from.Value, toId: to.Value, facilitatorId: null, cancellationToken: cancellationToken));
-        }
+        return Ok(PaginatedResult.Create(result.Value, null));
+    }
 
-        if (from.HasValue)
+    /// <summary>
+    /// Get connections between the authenticated user's selected party and the specified target party.
+    /// </summary>
+    [HttpGet("{id}")]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
+    [ProducesResponseType<ExtConnection>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromRoute] Guid id, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
+    {
+        var userUuid = Audit.Value;
+        
+        var result = await ConnectionService.Get(id, cancellationToken: cancellationToken);
+        if (result.Value.FromId == party || result.Value.ToId == party || result.Value.FacilitatorId == party)
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
user: login: jonkjetiloye
body: Fasilitator party skal ikke ha tilgang til å gjøre oppslag her. Det vil evt. være i ett Klientdelegerings API senere.
created_at: 2025-05-07T10:46:49Z
author_association: MEMBER
diff_hunk: @@ -22,281 +23,209 @@ namespace Altinn.AccessManagement.Api.Enduser.Controllers;
 [Route("accessmanagement/api/v1/enduser/connections")]
 [FeatureGate(AccessManagementEnduserFeatureFlags.ControllerConnections)]
 [Authorize(Policy = AuthzConstants.SCOPE_PORTAL_ENDUSER)]
-public class ConnectionController(IHttpContextAccessor accessor, IConnectionService connectionService, IAssignmentService assignmentService, IEntityRepository entityRepository) : ControllerBase
+public class ConnectionController(IHttpContextAccessor accessor, IEnduserConnectionService connectionService, IDbAudit audit) : ControllerBase
 {
     private IHttpContextAccessor Accessor { get; } = accessor;
 
+    private IEnduserConnectionService ConnectionService { get; } = connectionService;
+
+    private IDbAudit Audit { get; } = audit;
+
     /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
+    /// Get connections between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpGet]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<AssignmentExternal>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetConnections([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
+        var userUuid = Audit.Value;
+        if (ValidationRules.EnduserGetConnection(userUuid.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
+        var result = await ConnectionService.Get(from: connection.From.ConvertToUuid(userUuid.ChangedBy), to: connection.To.ConvertToUuid(userUuid.ChangedBy), cancellationToken: cancellationToken);
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
user: login: jonkjetiloye
body: Autentisert bruker skal ikke brukes direkte som from eller to i spørringer.

Det er alltid Party, From og To fra ConnectionInput som er det som skal brukes i oppslagene (som _kan_ være lik autentisert bruker)
created_at: 2025-05-07T10:54:46Z
author_association: MEMBER
diff_hunk: @@ -22,281 +23,209 @@ namespace Altinn.AccessManagement.Api.Enduser.Controllers;
 [Route("accessmanagement/api/v1/enduser/connections")]
 [FeatureGate(AccessManagementEnduserFeatureFlags.ControllerConnections)]
 [Authorize(Policy = AuthzConstants.SCOPE_PORTAL_ENDUSER)]
-public class ConnectionController(IHttpContextAccessor accessor, IConnectionService connectionService, IAssignmentService assignmentService, IEntityRepository entityRepository) : ControllerBase
+public class ConnectionController(IHttpContextAccessor accessor, IEnduserConnectionService connectionService, IDbAudit audit) : ControllerBase
 {
     private IHttpContextAccessor Accessor { get; } = accessor;
 
+    private IEnduserConnectionService ConnectionService { get; } = connectionService;
+
+    private IDbAudit Audit { get; } = audit;
+
     /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
+    /// Get connections between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpGet]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<AssignmentExternal>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetConnections([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
+        var userUuid = Audit.Value;
+        if (ValidationRules.EnduserGetConnection(userUuid.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
+        var result = await ConnectionService.Get(from: connection.From.ConvertToUuid(userUuid.ChangedBy), to: connection.To.ConvertToUuid(userUuid.ChangedBy), cancellationToken: cancellationToken);
+        if (result.IsProblem)
         {
-            // Party must match From or To
-            return BadRequest();
+            return result.Problem.ToActionResult();
         }
 
-        var partyUuid = Accessor.GetPartyUuid();
-        var audit = new ChangeRequestOptions()
-        {
-            ChangedBy = partyUuid,
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from.HasValue && to.HasValue)
-        {
-            return Ok(await connectionService.Get(fromId: from.Value, toId: to.Value, facilitatorId: null, cancellationToken: cancellationToken));
-        }
+        return Ok(PaginatedResult.Create(result.Value, null));
+    }
 
-        if (from.HasValue)
+    /// <summary>
+    /// Get connections between the authenticated user's selected party and the specified target party.
+    /// </summary>
+    [HttpGet("{id}")]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
+    [ProducesResponseType<ExtConnection>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromRoute] Guid id, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
+    {
+        var userUuid = Audit.Value;
+        
+        var result = await ConnectionService.Get(id, cancellationToken: cancellationToken);
+        if (result.Value.FromId == party || result.Value.ToId == party || result.Value.FacilitatorId == party)
         {
-            return Ok(await connectionService.GetReceived(from.Value, cancellationToken: cancellationToken));
+            return Ok(result.Value);
         }
 
-        if (to.HasValue)
-        {
-            return Ok(await connectionService.GetGiven(to.Value, cancellationToken: cancellationToken));
-        }
-        
-        return BadRequest();
+        return NotFound();
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
     [HttpPost]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, CancellationToken cancellationToken = default)
+    [ProducesResponseType<Assignment>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddAssignment([FromQuery] ConnectionInput connection, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from != party)
-        {
-            throw new Exception("From party does not match from");
-        }
-
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
-        {
-            return Problem("From party not found");
-        }
-
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("From must be of type 'Organisasjon'");
-        }
-
-        //// To must be Type:Organisasjon
-        //// #550:AC:Det skal bare være mulig å legge til Organisasjoner som ny Rightholder
-        var toEntity = await entityRepository.GetExtended(to);
-        if (toEntity == null)
+        var audit = Audit.Value;
+        if (ValidationRules.EnduserAddConnection(audit.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Problem("To party not found");
+            return problem.ToActionResult();
         }
 
-        if (!toEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("To must be of type 'Organisasjon'");
-        }
-
-        var res = await assignmentService.GetOrCreateAssignmentInternal(fromId: from, toId: to, roleCode: "rettighetshaver", options, cancellationToken: cancellationToken);
-
-        if (res != null)
+        var result = await ConnectionService.AddAssignment(connection.From.ConvertToUuid(audit.ChangedBy), connection.To.ConvertToUuid(audit.ChangedBy), "rettighetshaver", cancellationToken);
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
user: login: jonkjetiloye
body: også her bruk av audit.ChangedBy
created_at: 2025-05-07T10:57:32Z
author_association: MEMBER
diff_hunk: @@ -22,281 +23,209 @@ namespace Altinn.AccessManagement.Api.Enduser.Controllers;
 [Route("accessmanagement/api/v1/enduser/connections")]
 [FeatureGate(AccessManagementEnduserFeatureFlags.ControllerConnections)]
 [Authorize(Policy = AuthzConstants.SCOPE_PORTAL_ENDUSER)]
-public class ConnectionController(IHttpContextAccessor accessor, IConnectionService connectionService, IAssignmentService assignmentService, IEntityRepository entityRepository) : ControllerBase
+public class ConnectionController(IHttpContextAccessor accessor, IEnduserConnectionService connectionService, IDbAudit audit) : ControllerBase
 {
     private IHttpContextAccessor Accessor { get; } = accessor;
 
+    private IEnduserConnectionService ConnectionService { get; } = connectionService;
+
+    private IDbAudit Audit { get; } = audit;
+
     /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
+    /// Get connections between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpGet]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<AssignmentExternal>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetConnections([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
+        var userUuid = Audit.Value;
+        if (ValidationRules.EnduserGetConnection(userUuid.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
+        var result = await ConnectionService.Get(from: connection.From.ConvertToUuid(userUuid.ChangedBy), to: connection.To.ConvertToUuid(userUuid.ChangedBy), cancellationToken: cancellationToken);
+        if (result.IsProblem)
         {
-            // Party must match From or To
-            return BadRequest();
+            return result.Problem.ToActionResult();
         }
 
-        var partyUuid = Accessor.GetPartyUuid();
-        var audit = new ChangeRequestOptions()
-        {
-            ChangedBy = partyUuid,
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from.HasValue && to.HasValue)
-        {
-            return Ok(await connectionService.Get(fromId: from.Value, toId: to.Value, facilitatorId: null, cancellationToken: cancellationToken));
-        }
+        return Ok(PaginatedResult.Create(result.Value, null));
+    }
 
-        if (from.HasValue)
+    /// <summary>
+    /// Get connections between the authenticated user's selected party and the specified target party.
+    /// </summary>
+    [HttpGet("{id}")]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
+    [ProducesResponseType<ExtConnection>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromRoute] Guid id, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
+    {
+        var userUuid = Audit.Value;
+        
+        var result = await ConnectionService.Get(id, cancellationToken: cancellationToken);
+        if (result.Value.FromId == party || result.Value.ToId == party || result.Value.FacilitatorId == party)
         {
-            return Ok(await connectionService.GetReceived(from.Value, cancellationToken: cancellationToken));
+            return Ok(result.Value);
         }
 
-        if (to.HasValue)
-        {
-            return Ok(await connectionService.GetGiven(to.Value, cancellationToken: cancellationToken));
-        }
-        
-        return BadRequest();
+        return NotFound();
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
     [HttpPost]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, CancellationToken cancellationToken = default)
+    [ProducesResponseType<Assignment>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddAssignment([FromQuery] ConnectionInput connection, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from != party)
-        {
-            throw new Exception("From party does not match from");
-        }
-
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
-        {
-            return Problem("From party not found");
-        }
-
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("From must be of type 'Organisasjon'");
-        }
-
-        //// To must be Type:Organisasjon
-        //// #550:AC:Det skal bare være mulig å legge til Organisasjoner som ny Rightholder
-        var toEntity = await entityRepository.GetExtended(to);
-        if (toEntity == null)
+        var audit = Audit.Value;
+        if (ValidationRules.EnduserAddConnection(audit.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Problem("To party not found");
+            return problem.ToActionResult();
         }
 
-        if (!toEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("To must be of type 'Organisasjon'");
-        }
-
-        var res = await assignmentService.GetOrCreateAssignmentInternal(fromId: from, toId: to, roleCode: "rettighetshaver", options, cancellationToken: cancellationToken);
-
-        if (res != null)
+        var result = await ConnectionService.AddAssignment(connection.From.ConvertToUuid(audit.ChangedBy), connection.To.ConvertToUuid(audit.ChangedBy), "rettighetshaver", cancellationToken);
+        if (result.IsProblem)
         {
-            return Ok(res);
+            return result.Problem.ToActionResult();
         }
 
-        return Problem("Unable add connection");
+        return Ok(result.Value);
     }
 
     /// <summary>
     /// Remove package from connection (assignment or delegation)
     /// </summary>
     [HttpDelete]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<ProblemInstance> RemoveConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> RemoveAssignment([FromQuery] ConnectionInput connection, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
+        var audit = Audit.Value;
+        if (ValidationRules.EnduserRemoveConnection(audit.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (!(from == party) && !(to == party))
-        {
-            throw new Exception("From party does not match from or to");
+            return problem.ToActionResult();
         }
 
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
+        problem = await ConnectionService.RemoveAssignment(connection.From.ConvertToUuid(audit.ChangedBy), connection.To.ConvertToUuid(audit.ChangedBy), "rettighetshaver", cascade, cancellationToken);
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
user: login: jonkjetiloye
body: også her bruk av audit.ChangedBy
created_at: 2025-05-07T10:59:28Z
author_association: MEMBER
diff_hunk: @@ -22,281 +23,209 @@ namespace Altinn.AccessManagement.Api.Enduser.Controllers;
 [Route("accessmanagement/api/v1/enduser/connections")]
 [FeatureGate(AccessManagementEnduserFeatureFlags.ControllerConnections)]
 [Authorize(Policy = AuthzConstants.SCOPE_PORTAL_ENDUSER)]
-public class ConnectionController(IHttpContextAccessor accessor, IConnectionService connectionService, IAssignmentService assignmentService, IEntityRepository entityRepository) : ControllerBase
+public class ConnectionController(IHttpContextAccessor accessor, IEnduserConnectionService connectionService, IDbAudit audit) : ControllerBase
 {
     private IHttpContextAccessor Accessor { get; } = accessor;
 
+    private IEnduserConnectionService ConnectionService { get; } = connectionService;
+
+    private IDbAudit Audit { get; } = audit;
+
     /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
+    /// Get connections between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpGet]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<AssignmentExternal>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetConnections([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
+        var userUuid = Audit.Value;
+        if (ValidationRules.EnduserGetConnection(userUuid.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
+        var result = await ConnectionService.Get(from: connection.From.ConvertToUuid(userUuid.ChangedBy), to: connection.To.ConvertToUuid(userUuid.ChangedBy), cancellationToken: cancellationToken);
+        if (result.IsProblem)
         {
-            // Party must match From or To
-            return BadRequest();
+            return result.Problem.ToActionResult();
         }
 
-        var partyUuid = Accessor.GetPartyUuid();
-        var audit = new ChangeRequestOptions()
-        {
-            ChangedBy = partyUuid,
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from.HasValue && to.HasValue)
-        {
-            return Ok(await connectionService.Get(fromId: from.Value, toId: to.Value, facilitatorId: null, cancellationToken: cancellationToken));
-        }
+        return Ok(PaginatedResult.Create(result.Value, null));
+    }
 
-        if (from.HasValue)
+    /// <summary>
+    /// Get connections between the authenticated user's selected party and the specified target party.
+    /// </summary>
+    [HttpGet("{id}")]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
+    [ProducesResponseType<ExtConnection>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromRoute] Guid id, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
+    {
+        var userUuid = Audit.Value;
+        
+        var result = await ConnectionService.Get(id, cancellationToken: cancellationToken);
+        if (result.Value.FromId == party || result.Value.ToId == party || result.Value.FacilitatorId == party)
         {
-            return Ok(await connectionService.GetReceived(from.Value, cancellationToken: cancellationToken));
+            return Ok(result.Value);
         }
 
-        if (to.HasValue)
-        {
-            return Ok(await connectionService.GetGiven(to.Value, cancellationToken: cancellationToken));
-        }
-        
-        return BadRequest();
+        return NotFound();
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
     [HttpPost]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, CancellationToken cancellationToken = default)
+    [ProducesResponseType<Assignment>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddAssignment([FromQuery] ConnectionInput connection, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from != party)
-        {
-            throw new Exception("From party does not match from");
-        }
-
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
-        {
-            return Problem("From party not found");
-        }
-
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("From must be of type 'Organisasjon'");
-        }
-
-        //// To must be Type:Organisasjon
-        //// #550:AC:Det skal bare være mulig å legge til Organisasjoner som ny Rightholder
-        var toEntity = await entityRepository.GetExtended(to);
-        if (toEntity == null)
+        var audit = Audit.Value;
+        if (ValidationRules.EnduserAddConnection(audit.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Problem("To party not found");
+            return problem.ToActionResult();
         }
 
-        if (!toEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("To must be of type 'Organisasjon'");
-        }
-
-        var res = await assignmentService.GetOrCreateAssignmentInternal(fromId: from, toId: to, roleCode: "rettighetshaver", options, cancellationToken: cancellationToken);
-
-        if (res != null)
+        var result = await ConnectionService.AddAssignment(connection.From.ConvertToUuid(audit.ChangedBy), connection.To.ConvertToUuid(audit.ChangedBy), "rettighetshaver", cancellationToken);
+        if (result.IsProblem)
         {
-            return Ok(res);
+            return result.Problem.ToActionResult();
         }
 
-        return Problem("Unable add connection");
+        return Ok(result.Value);
     }
 
     /// <summary>
     /// Remove package from connection (assignment or delegation)
     /// </summary>
     [HttpDelete]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<ProblemInstance> RemoveConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> RemoveAssignment([FromQuery] ConnectionInput connection, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
+        var audit = Audit.Value;
+        if (ValidationRules.EnduserRemoveConnection(audit.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (!(from == party) && !(to == party))
-        {
-            throw new Exception("From party does not match from or to");
+            return problem.ToActionResult();
         }
 
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
+        problem = await ConnectionService.RemoveAssignment(connection.From.ConvertToUuid(audit.ChangedBy), connection.To.ConvertToUuid(audit.ChangedBy), "rettighetshaver", cascade, cancellationToken);
+        if (problem is { })
         {
-            throw new Exception("From party not found");
+            problem.ToActionResult();
         }
 
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            throw new Exception("From must be of type 'Organisasjon'");
-        }
-
-        return await assignmentService.DeleteAssignment(fromId: from, toId: to, roleCode: "rettighetshaver", options, cascade: cascade, cancellationToken);
+        return NoContent();
     }
 
     /// <summary>
     /// Creates an assignment between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
-    [HttpGet]
-    [Route("accesspackages")]
+    [HttpGet("accesspackages")]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<ConnectionPackage>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetPackages([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetPackages([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
+        var partyUuid = Accessor.GetPartyUuid();
+        if (ValidationRules.EnduserGetConnection(partyUuid, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
+        var result = await ConnectionService.GetPackages(connection.From.ConvertToUuid(partyUuid), connection.To.ConvertToUuid(partyUuid), cancellationToken);
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
user: login: jonkjetiloye
body: Oppslag må gjøres basert på ConnectionInput 
created_at: 2025-05-07T11:00:46Z
author_association: MEMBER
diff_hunk: @@ -22,281 +23,209 @@ namespace Altinn.AccessManagement.Api.Enduser.Controllers;
 [Route("accessmanagement/api/v1/enduser/connections")]
 [FeatureGate(AccessManagementEnduserFeatureFlags.ControllerConnections)]
 [Authorize(Policy = AuthzConstants.SCOPE_PORTAL_ENDUSER)]
-public class ConnectionController(IHttpContextAccessor accessor, IConnectionService connectionService, IAssignmentService assignmentService, IEntityRepository entityRepository) : ControllerBase
+public class ConnectionController(IHttpContextAccessor accessor, IEnduserConnectionService connectionService, IDbAudit audit) : ControllerBase
 {
     private IHttpContextAccessor Accessor { get; } = accessor;
 
+    private IEnduserConnectionService ConnectionService { get; } = connectionService;
+
+    private IDbAudit Audit { get; } = audit;
+
     /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
+    /// Get connections between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpGet]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<AssignmentExternal>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetConnections([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
+        var userUuid = Audit.Value;
+        if (ValidationRules.EnduserGetConnection(userUuid.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
+        var result = await ConnectionService.Get(from: connection.From.ConvertToUuid(userUuid.ChangedBy), to: connection.To.ConvertToUuid(userUuid.ChangedBy), cancellationToken: cancellationToken);
+        if (result.IsProblem)
         {
-            // Party must match From or To
-            return BadRequest();
+            return result.Problem.ToActionResult();
         }
 
-        var partyUuid = Accessor.GetPartyUuid();
-        var audit = new ChangeRequestOptions()
-        {
-            ChangedBy = partyUuid,
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from.HasValue && to.HasValue)
-        {
-            return Ok(await connectionService.Get(fromId: from.Value, toId: to.Value, facilitatorId: null, cancellationToken: cancellationToken));
-        }
+        return Ok(PaginatedResult.Create(result.Value, null));
+    }
 
-        if (from.HasValue)
+    /// <summary>
+    /// Get connections between the authenticated user's selected party and the specified target party.
+    /// </summary>
+    [HttpGet("{id}")]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
+    [ProducesResponseType<ExtConnection>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromRoute] Guid id, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
+    {
+        var userUuid = Audit.Value;
+        
+        var result = await ConnectionService.Get(id, cancellationToken: cancellationToken);
+        if (result.Value.FromId == party || result.Value.ToId == party || result.Value.FacilitatorId == party)
         {
-            return Ok(await connectionService.GetReceived(from.Value, cancellationToken: cancellationToken));
+            return Ok(result.Value);
         }
 
-        if (to.HasValue)
-        {
-            return Ok(await connectionService.GetGiven(to.Value, cancellationToken: cancellationToken));
-        }
-        
-        return BadRequest();
+        return NotFound();
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
     [HttpPost]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, CancellationToken cancellationToken = default)
+    [ProducesResponseType<Assignment>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddAssignment([FromQuery] ConnectionInput connection, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from != party)
-        {
-            throw new Exception("From party does not match from");
-        }
-
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
-        {
-            return Problem("From party not found");
-        }
-
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("From must be of type 'Organisasjon'");
-        }
-
-        //// To must be Type:Organisasjon
-        //// #550:AC:Det skal bare være mulig å legge til Organisasjoner som ny Rightholder
-        var toEntity = await entityRepository.GetExtended(to);
-        if (toEntity == null)
+        var audit = Audit.Value;
+        if (ValidationRules.EnduserAddConnection(audit.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Problem("To party not found");
+            return problem.ToActionResult();
         }
 
-        if (!toEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("To must be of type 'Organisasjon'");
-        }
-
-        var res = await assignmentService.GetOrCreateAssignmentInternal(fromId: from, toId: to, roleCode: "rettighetshaver", options, cancellationToken: cancellationToken);
-
-        if (res != null)
+        var result = await ConnectionService.AddAssignment(connection.From.ConvertToUuid(audit.ChangedBy), connection.To.ConvertToUuid(audit.ChangedBy), "rettighetshaver", cancellationToken);
+        if (result.IsProblem)
         {
-            return Ok(res);
+            return result.Problem.ToActionResult();
         }
 
-        return Problem("Unable add connection");
+        return Ok(result.Value);
     }
 
     /// <summary>
     /// Remove package from connection (assignment or delegation)
     /// </summary>
     [HttpDelete]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<ProblemInstance> RemoveConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> RemoveAssignment([FromQuery] ConnectionInput connection, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
+        var audit = Audit.Value;
+        if (ValidationRules.EnduserRemoveConnection(audit.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (!(from == party) && !(to == party))
-        {
-            throw new Exception("From party does not match from or to");
+            return problem.ToActionResult();
         }
 
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
+        problem = await ConnectionService.RemoveAssignment(connection.From.ConvertToUuid(audit.ChangedBy), connection.To.ConvertToUuid(audit.ChangedBy), "rettighetshaver", cascade, cancellationToken);
+        if (problem is { })
         {
-            throw new Exception("From party not found");
+            problem.ToActionResult();
         }
 
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            throw new Exception("From must be of type 'Organisasjon'");
-        }
-
-        return await assignmentService.DeleteAssignment(fromId: from, toId: to, roleCode: "rettighetshaver", options, cascade: cascade, cancellationToken);
+        return NoContent();
     }
 
     /// <summary>
     /// Creates an assignment between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
-    [HttpGet]
-    [Route("accesspackages")]
+    [HttpGet("accesspackages")]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<ConnectionPackage>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetPackages([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetPackages([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
+        var partyUuid = Accessor.GetPartyUuid();
+        if (ValidationRules.EnduserGetConnection(partyUuid, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
+        var result = await ConnectionService.GetPackages(connection.From.ConvertToUuid(partyUuid), connection.To.ConvertToUuid(partyUuid), cancellationToken);
+        if (result.IsProblem)
         {
-            // Party must match From or To
-            return BadRequest();
+            return result.Problem.ToActionResult();
         }
 
-        var res = await connectionService.GetPackages(fromId: from, toId: to);
-
-        return Ok(res);
+        return Ok(PaginatedResult.Create(result.Value, null));
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
-    [HttpPost]
-    [Route("accesspackages")]
+    [HttpPost("accesspackages")]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddPackages([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] Guid? package, [FromQuery] string packageUrn, CancellationToken cancellationToken = default)
+    [ProducesResponseType<AssignmentPackage>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddPackages([FromQuery] ConnectionInput connection, [FromQuery] Guid? packageId, [FromQuery] string packageUrn, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
+        throw new NotImplementedException();
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
user: login: jonkjetiloye
body: NotImplementedException? 🤔 
created_at: 2025-05-07T11:05:49Z
author_association: MEMBER
diff_hunk: @@ -22,281 +23,209 @@ namespace Altinn.AccessManagement.Api.Enduser.Controllers;
 [Route("accessmanagement/api/v1/enduser/connections")]
 [FeatureGate(AccessManagementEnduserFeatureFlags.ControllerConnections)]
 [Authorize(Policy = AuthzConstants.SCOPE_PORTAL_ENDUSER)]
-public class ConnectionController(IHttpContextAccessor accessor, IConnectionService connectionService, IAssignmentService assignmentService, IEntityRepository entityRepository) : ControllerBase
+public class ConnectionController(IHttpContextAccessor accessor, IEnduserConnectionService connectionService, IDbAudit audit) : ControllerBase
 {
     private IHttpContextAccessor Accessor { get; } = accessor;
 
+    private IEnduserConnectionService ConnectionService { get; } = connectionService;
+
+    private IDbAudit Audit { get; } = audit;
+
     /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
+    /// Get connections between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpGet]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<AssignmentExternal>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetConnections([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
+        var userUuid = Audit.Value;
+        if (ValidationRules.EnduserGetConnection(userUuid.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
+        var result = await ConnectionService.Get(from: connection.From.ConvertToUuid(userUuid.ChangedBy), to: connection.To.ConvertToUuid(userUuid.ChangedBy), cancellationToken: cancellationToken);
+        if (result.IsProblem)
         {
-            // Party must match From or To
-            return BadRequest();
+            return result.Problem.ToActionResult();
         }
 
-        var partyUuid = Accessor.GetPartyUuid();
-        var audit = new ChangeRequestOptions()
-        {
-            ChangedBy = partyUuid,
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from.HasValue && to.HasValue)
-        {
-            return Ok(await connectionService.Get(fromId: from.Value, toId: to.Value, facilitatorId: null, cancellationToken: cancellationToken));
-        }
+        return Ok(PaginatedResult.Create(result.Value, null));
+    }
 
-        if (from.HasValue)
+    /// <summary>
+    /// Get connections between the authenticated user's selected party and the specified target party.
+    /// </summary>
+    [HttpGet("{id}")]
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
user: login: andreasisnes
body: @Thuen 
created_at: 2025-05-07T11:14:13Z
author_association: COLLABORATOR
diff_hunk: @@ -22,281 +23,209 @@ namespace Altinn.AccessManagement.Api.Enduser.Controllers;
 [Route("accessmanagement/api/v1/enduser/connections")]
 [FeatureGate(AccessManagementEnduserFeatureFlags.ControllerConnections)]
 [Authorize(Policy = AuthzConstants.SCOPE_PORTAL_ENDUSER)]
-public class ConnectionController(IHttpContextAccessor accessor, IConnectionService connectionService, IAssignmentService assignmentService, IEntityRepository entityRepository) : ControllerBase
+public class ConnectionController(IHttpContextAccessor accessor, IEnduserConnectionService connectionService, IDbAudit audit) : ControllerBase
 {
     private IHttpContextAccessor Accessor { get; } = accessor;
 
+    private IEnduserConnectionService ConnectionService { get; } = connectionService;
+
+    private IDbAudit Audit { get; } = audit;
+
     /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
+    /// Get connections between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpGet]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<AssignmentExternal>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetConnections([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
+        var userUuid = Audit.Value;
+        if (ValidationRules.EnduserGetConnection(userUuid.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
+        var result = await ConnectionService.Get(from: connection.From.ConvertToUuid(userUuid.ChangedBy), to: connection.To.ConvertToUuid(userUuid.ChangedBy), cancellationToken: cancellationToken);
+        if (result.IsProblem)
         {
-            // Party must match From or To
-            return BadRequest();
+            return result.Problem.ToActionResult();
         }
 
-        var partyUuid = Accessor.GetPartyUuid();
-        var audit = new ChangeRequestOptions()
-        {
-            ChangedBy = partyUuid,
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from.HasValue && to.HasValue)
-        {
-            return Ok(await connectionService.Get(fromId: from.Value, toId: to.Value, facilitatorId: null, cancellationToken: cancellationToken));
-        }
+        return Ok(PaginatedResult.Create(result.Value, null));
+    }
 
-        if (from.HasValue)
+    /// <summary>
+    /// Get connections between the authenticated user's selected party and the specified target party.
+    /// </summary>
+    [HttpGet("{id}")]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
+    [ProducesResponseType<ExtConnection>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromRoute] Guid id, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
+    {
+        var userUuid = Audit.Value;
+        
+        var result = await ConnectionService.Get(id, cancellationToken: cancellationToken);
+        if (result.Value.FromId == party || result.Value.ToId == party || result.Value.FacilitatorId == party)
         {
-            return Ok(await connectionService.GetReceived(from.Value, cancellationToken: cancellationToken));
+            return Ok(result.Value);
         }
 
-        if (to.HasValue)
-        {
-            return Ok(await connectionService.GetGiven(to.Value, cancellationToken: cancellationToken));
-        }
-        
-        return BadRequest();
+        return NotFound();
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
     [HttpPost]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, CancellationToken cancellationToken = default)
+    [ProducesResponseType<Assignment>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddAssignment([FromQuery] ConnectionInput connection, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from != party)
-        {
-            throw new Exception("From party does not match from");
-        }
-
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
-        {
-            return Problem("From party not found");
-        }
-
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("From must be of type 'Organisasjon'");
-        }
-
-        //// To must be Type:Organisasjon
-        //// #550:AC:Det skal bare være mulig å legge til Organisasjoner som ny Rightholder
-        var toEntity = await entityRepository.GetExtended(to);
-        if (toEntity == null)
+        var audit = Audit.Value;
+        if (ValidationRules.EnduserAddConnection(audit.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Problem("To party not found");
+            return problem.ToActionResult();
         }
 
-        if (!toEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("To must be of type 'Organisasjon'");
-        }
-
-        var res = await assignmentService.GetOrCreateAssignmentInternal(fromId: from, toId: to, roleCode: "rettighetshaver", options, cancellationToken: cancellationToken);
-
-        if (res != null)
+        var result = await ConnectionService.AddAssignment(connection.From.ConvertToUuid(audit.ChangedBy), connection.To.ConvertToUuid(audit.ChangedBy), "rettighetshaver", cancellationToken);
+        if (result.IsProblem)
         {
-            return Ok(res);
+            return result.Problem.ToActionResult();
         }
 
-        return Problem("Unable add connection");
+        return Ok(result.Value);
     }
 
     /// <summary>
     /// Remove package from connection (assignment or delegation)
     /// </summary>
     [HttpDelete]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<ProblemInstance> RemoveConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> RemoveAssignment([FromQuery] ConnectionInput connection, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
+        var audit = Audit.Value;
+        if (ValidationRules.EnduserRemoveConnection(audit.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (!(from == party) && !(to == party))
-        {
-            throw new Exception("From party does not match from or to");
+            return problem.ToActionResult();
         }
 
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
+        problem = await ConnectionService.RemoveAssignment(connection.From.ConvertToUuid(audit.ChangedBy), connection.To.ConvertToUuid(audit.ChangedBy), "rettighetshaver", cascade, cancellationToken);
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
user: login: andreasisnes
body: den fallbvacker til changedBy hvis from eller to er "me"
created_at: 2025-05-07T11:16:43Z
author_association: COLLABORATOR
diff_hunk: @@ -22,281 +23,209 @@ namespace Altinn.AccessManagement.Api.Enduser.Controllers;
 [Route("accessmanagement/api/v1/enduser/connections")]
 [FeatureGate(AccessManagementEnduserFeatureFlags.ControllerConnections)]
 [Authorize(Policy = AuthzConstants.SCOPE_PORTAL_ENDUSER)]
-public class ConnectionController(IHttpContextAccessor accessor, IConnectionService connectionService, IAssignmentService assignmentService, IEntityRepository entityRepository) : ControllerBase
+public class ConnectionController(IHttpContextAccessor accessor, IEnduserConnectionService connectionService, IDbAudit audit) : ControllerBase
 {
     private IHttpContextAccessor Accessor { get; } = accessor;
 
+    private IEnduserConnectionService ConnectionService { get; } = connectionService;
+
+    private IDbAudit Audit { get; } = audit;
+
     /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
+    /// Get connections between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpGet]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<AssignmentExternal>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetConnections([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
+        var userUuid = Audit.Value;
+        if (ValidationRules.EnduserGetConnection(userUuid.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
+        var result = await ConnectionService.Get(from: connection.From.ConvertToUuid(userUuid.ChangedBy), to: connection.To.ConvertToUuid(userUuid.ChangedBy), cancellationToken: cancellationToken);
+        if (result.IsProblem)
         {
-            // Party must match From or To
-            return BadRequest();
+            return result.Problem.ToActionResult();
         }
 
-        var partyUuid = Accessor.GetPartyUuid();
-        var audit = new ChangeRequestOptions()
-        {
-            ChangedBy = partyUuid,
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from.HasValue && to.HasValue)
-        {
-            return Ok(await connectionService.Get(fromId: from.Value, toId: to.Value, facilitatorId: null, cancellationToken: cancellationToken));
-        }
+        return Ok(PaginatedResult.Create(result.Value, null));
+    }
 
-        if (from.HasValue)
+    /// <summary>
+    /// Get connections between the authenticated user's selected party and the specified target party.
+    /// </summary>
+    [HttpGet("{id}")]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
+    [ProducesResponseType<ExtConnection>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromRoute] Guid id, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
+    {
+        var userUuid = Audit.Value;
+        
+        var result = await ConnectionService.Get(id, cancellationToken: cancellationToken);
+        if (result.Value.FromId == party || result.Value.ToId == party || result.Value.FacilitatorId == party)
         {
-            return Ok(await connectionService.GetReceived(from.Value, cancellationToken: cancellationToken));
+            return Ok(result.Value);
         }
 
-        if (to.HasValue)
-        {
-            return Ok(await connectionService.GetGiven(to.Value, cancellationToken: cancellationToken));
-        }
-        
-        return BadRequest();
+        return NotFound();
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
     [HttpPost]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, CancellationToken cancellationToken = default)
+    [ProducesResponseType<Assignment>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddAssignment([FromQuery] ConnectionInput connection, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from != party)
-        {
-            throw new Exception("From party does not match from");
-        }
-
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
-        {
-            return Problem("From party not found");
-        }
-
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("From must be of type 'Organisasjon'");
-        }
-
-        //// To must be Type:Organisasjon
-        //// #550:AC:Det skal bare være mulig å legge til Organisasjoner som ny Rightholder
-        var toEntity = await entityRepository.GetExtended(to);
-        if (toEntity == null)
+        var audit = Audit.Value;
+        if (ValidationRules.EnduserAddConnection(audit.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Problem("To party not found");
+            return problem.ToActionResult();
         }
 
-        if (!toEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("To must be of type 'Organisasjon'");
-        }
-
-        var res = await assignmentService.GetOrCreateAssignmentInternal(fromId: from, toId: to, roleCode: "rettighetshaver", options, cancellationToken: cancellationToken);
-
-        if (res != null)
+        var result = await ConnectionService.AddAssignment(connection.From.ConvertToUuid(audit.ChangedBy), connection.To.ConvertToUuid(audit.ChangedBy), "rettighetshaver", cancellationToken);
+        if (result.IsProblem)
         {
-            return Ok(res);
+            return result.Problem.ToActionResult();
         }
 
-        return Problem("Unable add connection");
+        return Ok(result.Value);
     }
 
     /// <summary>
     /// Remove package from connection (assignment or delegation)
     /// </summary>
     [HttpDelete]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<ProblemInstance> RemoveConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> RemoveAssignment([FromQuery] ConnectionInput connection, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
+        var audit = Audit.Value;
+        if (ValidationRules.EnduserRemoveConnection(audit.ChangedBy, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (!(from == party) && !(to == party))
-        {
-            throw new Exception("From party does not match from or to");
+            return problem.ToActionResult();
         }
 
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
+        problem = await ConnectionService.RemoveAssignment(connection.From.ConvertToUuid(audit.ChangedBy), connection.To.ConvertToUuid(audit.ChangedBy), "rettighetshaver", cascade, cancellationToken);
+        if (problem is { })
         {
-            throw new Exception("From party not found");
+            problem.ToActionResult();
         }
 
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            throw new Exception("From must be of type 'Organisasjon'");
-        }
-
-        return await assignmentService.DeleteAssignment(fromId: from, toId: to, roleCode: "rettighetshaver", options, cascade: cascade, cancellationToken);
+        return NoContent();
     }
 
     /// <summary>
     /// Creates an assignment between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
-    [HttpGet]
-    [Route("accesspackages")]
+    [HttpGet("accesspackages")]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<ConnectionPackage>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetPackages([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetPackages([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
+        var partyUuid = Accessor.GetPartyUuid();
+        if (ValidationRules.EnduserGetConnection(partyUuid, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
+        var result = await ConnectionService.GetPackages(connection.From.ConvertToUuid(partyUuid), connection.To.ConvertToUuid(partyUuid), cancellationToken);
+        if (result.IsProblem)
         {
-            // Party must match From or To
-            return BadRequest();
+            return result.Problem.ToActionResult();
         }
 
-        var res = await connectionService.GetPackages(fromId: from, toId: to);
-
-        return Ok(res);
+        return Ok(PaginatedResult.Create(result.Value, null));
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
-    [HttpPost]
-    [Route("accesspackages")]
+    [HttpPost("accesspackages")]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddPackages([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] Guid? package, [FromQuery] string packageUrn, CancellationToken cancellationToken = default)
+    [ProducesResponseType<AssignmentPackage>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddPackages([FromQuery] ConnectionInput connection, [FromQuery] Guid? packageId, [FromQuery] string packageUrn, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
+        throw new NotImplementedException();
 
-        if (from != party)
+        var useruuid = Accessor.GetPartyUuid();
+        if (ValidationRules.EnduserAddConnection(useruuid, connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            // Party must match From or To
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        //// From must by Type:Organisasjon
-        //// #568:AC:From party må være en Organisasjon (skal ikke være mulig å delegere fra privatperson el. andre entitetstyper enda)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
+        async Task<Result<AssignmentPackage>> AddPackage()
         {
-            throw new Exception("From party not found");
-        }
-
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            throw new Exception("From must be of type 'Organisasjon'");
-        }
-
-        //// To must be Type:Organisasjon
-        //// #568:AC:To party må være en Organisasjon (skal ikke være mulig å delegere fra privatperson el. andre entitetstyper enda)
-        var toEntity = await entityRepository.GetExtended(to);
-        if (toEntity == null)
-        {
-            return Problem("To party not found");
-        }
-
-        if (!toEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("To must be of type 'Organisasjon'");
-        }
-
-        if (package.HasValue)
-        {
-            var res = await connectionService.AddPackage(fromId: from, toId: to, roleCode: "rettighetshaver", packageId: package.Value, options);
-            if (res)
+            if (packageId.HasValue)
             {
-                return Ok();
+                return await ConnectionService.AddPackage(connection.From.ConvertToUuid(useruuid), connection.To.ConvertToUuid(useruuid), "rettighetshaver", packageId.Value, cancellationToken);
             }
+
+            return await ConnectionService.AddPackage(connection.From.ConvertToUuid(useruuid), connection.To.ConvertToUuid(useruuid), "rettighetshaver", packageUrn, cancellationToken);
         }
-        else
+
+        var result = await AddPackage();
+        if (result.IsProblem)
         {
-            packageUrn = packageUrn.ToLower().StartsWith("urn:") ? packageUrn : ":" + packageUrn;
-            var res = await connectionService.AddPackage(fromId: from, toId: to, roleCode: "rettighetshaver", packageUrn: packageUrn, options);
-            if (res)
-            {
-                return Ok();
-            }
+            result.Problem.ToActionResult();
         }
 
-        return Problem("Unable to remove package");
+        return Ok(result.Value);
     }
 
     /// <summary>
     /// Remove package from connection (assignment or delegation)
     /// </summary>
-    [HttpDelete]
-    [Route("accesspackages")]
+    [HttpDelete("accesspackages")]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> RemovePackages([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] Guid package, CancellationToken cancellationToken = default)
+    [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> RemovePackages([FromQuery] ConnectionInput connection, [FromQuery] Guid package, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
+        throw new NotImplementedException();
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
user: login: jonkjetiloye
body: NotImplementedException? 🤔
created_at: 2025-05-07T11:39:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,538 @@
+using Altinn.AccessMgmt.Core.Models;
+using Altinn.Authorization.ProblemDetails;
+
+namespace Altinn.AccessManagement.Core.Errors;
+
+/// <summary>
+/// A utility class that provides methods for validating data using a series of rules.
+/// </summary>
+public static class ValidationRules
+{
+    /// <summary>
+    /// A delegate representing a validation rule that accepts a reference to a <see cref="ValidationErrorBuilder"/>
+    /// and adds errors if validation fails.
+    /// </summary>
+    /// <param name="errors">The reference to the <see cref="ValidationErrorBuilder"/> where validation errors are added.</param>
+    public delegate void Func(ref ValidationErrorBuilder errors);
+
+    /// <summary>
+    /// A delegate that returns a validation rule that accepts a reference to a <see cref="ValidationErrorBuilder"/>
+    /// and adds errors if validation fails.
+    /// </summary>
+    public delegate Func FuncExpression();
+
+    /// <summary>
+    /// Validates a series of rules against the provided parameters.
+    /// It executes each rule in sequence and returns a <see cref="ValidationProblemInstance"/>
+    /// if any validation errors are found, or <c>null</c> if no errors exist.
+    /// </summary>
+    /// <param name="rules">An array of validation rules to apply.</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> containing validation errors, if any,
+    /// or <c>null</c> if the validation passed without any errors.
+    /// </returns>
+    public static ValidationProblemInstance? Validate(params FuncExpression[] rules)
+    {
+        var builder = default(ValidationErrorBuilder);
+        All(rules)()(ref builder);
+        builder.TryBuild(out var result);
+        return result;
+    }
+
+    /// <summary>
+    /// Validates a series of rules against the provided parameters.
+    /// It executes each rule in sequence and returns a <see cref="ValidationProblemInstance"/>
+    /// if any validation errors are found, or <c>null</c> if no errors exist.
+    /// </summary>
+    /// <param name="rules">An array of validation rules to apply.</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> containing validation errors, if any,
+    /// or <c>null</c> if the validation passed without any errors.
+    /// </returns>
+    public static ValidationProblemInstance? Validate(params Func[] rules)
+    {
+        var builder = default(ValidationErrorBuilder);
+        foreach (var rule in rules)
+        {
+            rule(ref builder);
+        }
+
+        builder.TryBuild(out var result);
+
+        return result;
+    }
+
+    /// <summary>
+    /// Combines multiple validation rules that must all pass.
+    /// </summary>
+    /// <param name="funcs">The validation functions to combine.</param>
+    /// <returns>A combined validation rule that applies all the specified rules.</returns>
+    public static FuncExpression All(params FuncExpression[] funcs) => () =>
+    {
+        var results = new List<Func>();
+        foreach (var func in funcs)
+        {
+            if (func() is var fn && fn is { })
+            {
+                results.Add(fn);
+            }
+        }
+
+        return (ref ValidationErrorBuilder errors) =>
+        {
+            foreach (var result in results)
+            {
+                result(ref errors);
+            }
+        };
+    };
+
+    /// <summary>
+    /// Combines multiple validation rules where at least one must pass.
+    /// </summary>
+    /// <param name="funcs">The validation functions to combine.</param>
+    /// <returns>A combined validation rule that applies any of the specified rules.</returns>
+    public static FuncExpression Any(params FuncExpression[] funcs) => () =>
+    {
+        var results = new List<Func>();
+        foreach (var func in funcs)
+        {
+            if (func() is var fn && fn is { })
+            {
+                results.Add(fn);
+            }
+        }
+
+        if (results.Count == funcs.Length)
+        {
+            return (ref ValidationErrorBuilder errors) =>
+            {
+                foreach (var result in results)
+                {
+                    result(ref errors);
+                }
+            };
+        }
+
+        return null;
+    };
+
+    /// <summary>
+    /// Validates the default query parameters for an end user.
+    /// </summary>
+    /// <param name="userUuid">The UUID of the user performing the action (current user).</param>
+    /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+    /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+    /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> with validation errors if any.
+    /// </returns>
+    public static ValidationProblemInstance EnduserAddConnection(Guid userUuid, string party, string from, string to) => Validate(
+        QueryParameters.Party(party),
+        Any(
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/EnduserValidationRules.cs
user: login: jonkjetiloye
body: På alle Add/Remove må både From og To være obligatoriske. Er bare GET oppslagene hvor en av de kan utelates.
created_at: 2025-05-07T11:47:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,538 @@
+using Altinn.AccessMgmt.Core.Models;
+using Altinn.Authorization.ProblemDetails;
+
+namespace Altinn.AccessManagement.Core.Errors;
+
+/// <summary>
+/// A utility class that provides methods for validating data using a series of rules.
+/// </summary>
+public static class ValidationRules
+{
+    /// <summary>
+    /// A delegate representing a validation rule that accepts a reference to a <see cref="ValidationErrorBuilder"/>
+    /// and adds errors if validation fails.
+    /// </summary>
+    /// <param name="errors">The reference to the <see cref="ValidationErrorBuilder"/> where validation errors are added.</param>
+    public delegate void Func(ref ValidationErrorBuilder errors);
+
+    /// <summary>
+    /// A delegate that returns a validation rule that accepts a reference to a <see cref="ValidationErrorBuilder"/>
+    /// and adds errors if validation fails.
+    /// </summary>
+    public delegate Func FuncExpression();
+
+    /// <summary>
+    /// Validates a series of rules against the provided parameters.
+    /// It executes each rule in sequence and returns a <see cref="ValidationProblemInstance"/>
+    /// if any validation errors are found, or <c>null</c> if no errors exist.
+    /// </summary>
+    /// <param name="rules">An array of validation rules to apply.</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> containing validation errors, if any,
+    /// or <c>null</c> if the validation passed without any errors.
+    /// </returns>
+    public static ValidationProblemInstance? Validate(params FuncExpression[] rules)
+    {
+        var builder = default(ValidationErrorBuilder);
+        All(rules)()(ref builder);
+        builder.TryBuild(out var result);
+        return result;
+    }
+
+    /// <summary>
+    /// Validates a series of rules against the provided parameters.
+    /// It executes each rule in sequence and returns a <see cref="ValidationProblemInstance"/>
+    /// if any validation errors are found, or <c>null</c> if no errors exist.
+    /// </summary>
+    /// <param name="rules">An array of validation rules to apply.</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> containing validation errors, if any,
+    /// or <c>null</c> if the validation passed without any errors.
+    /// </returns>
+    public static ValidationProblemInstance? Validate(params Func[] rules)
+    {
+        var builder = default(ValidationErrorBuilder);
+        foreach (var rule in rules)
+        {
+            rule(ref builder);
+        }
+
+        builder.TryBuild(out var result);
+
+        return result;
+    }
+
+    /// <summary>
+    /// Combines multiple validation rules that must all pass.
+    /// </summary>
+    /// <param name="funcs">The validation functions to combine.</param>
+    /// <returns>A combined validation rule that applies all the specified rules.</returns>
+    public static FuncExpression All(params FuncExpression[] funcs) => () =>
+    {
+        var results = new List<Func>();
+        foreach (var func in funcs)
+        {
+            if (func() is var fn && fn is { })
+            {
+                results.Add(fn);
+            }
+        }
+
+        return (ref ValidationErrorBuilder errors) =>
+        {
+            foreach (var result in results)
+            {
+                result(ref errors);
+            }
+        };
+    };
+
+    /// <summary>
+    /// Combines multiple validation rules where at least one must pass.
+    /// </summary>
+    /// <param name="funcs">The validation functions to combine.</param>
+    /// <returns>A combined validation rule that applies any of the specified rules.</returns>
+    public static FuncExpression Any(params FuncExpression[] funcs) => () =>
+    {
+        var results = new List<Func>();
+        foreach (var func in funcs)
+        {
+            if (func() is var fn && fn is { })
+            {
+                results.Add(fn);
+            }
+        }
+
+        if (results.Count == funcs.Length)
+        {
+            return (ref ValidationErrorBuilder errors) =>
+            {
+                foreach (var result in results)
+                {
+                    result(ref errors);
+                }
+            };
+        }
+
+        return null;
+    };
+
+    /// <summary>
+    /// Validates the default query parameters for an end user.
+    /// </summary>
+    /// <param name="userUuid">The UUID of the user performing the action (current user).</param>
+    /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+    /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+    /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> with validation errors if any.
+    /// </returns>
+    public static ValidationProblemInstance EnduserAddConnection(Guid userUuid, string party, string from, string to) => Validate(
+        QueryParameters.Party(party),
+        Any(
+            QueryParameters.PartyFrom(from),
+            QueryParameters.PartyTo(to)
+        ),
+        QueryParameters.EnduserCreateCombination(userUuid, party, from, to)
+    );
+
+    /// <summary>
+    /// Validates the default query parameters for an end user.
+    /// </summary>
+    /// <param name="userUuid">The UUID of the user performing the action (current user).</param>
+    /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+    /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+    /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> with validation errors if any.
+    /// </returns>
+    public static ValidationProblemInstance EnduserGetConnection(Guid userUuid, string party, string from, string to) => Validate(
+        QueryParameters.Party(party),
+        QueryParameters.EnduserReadInputCombination(userUuid, party, from, to),
+        Any(
+            QueryParameters.PartyFrom(from),
+            QueryParameters.PartyTo(to)
+        )
+    );
+
+    /// <summary>
+    /// Validates the default query parameters for an end user.
+    /// </summary>
+    /// <param name="userUuid">The UUID of the user performing the action (current user).</param>
+    /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+    /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+    /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> with validation errors if any.
+    /// </returns>
+    public static ValidationProblemInstance EnduserRemoveConnection(Guid userUuid, string party, string from, string to) => Validate(
+        QueryParameters.Party(party),
+        Any(
+            QueryParameters.PartyFrom(from),
+            QueryParameters.PartyTo(to)
+        ),
+        QueryParameters.EnduserDeleteCombination(userUuid, party, from, to)
+    );
+
+    /// <summary>
+    /// Provides validation rules for query parameters.
+    /// </summary>
+    public static class QueryParameters
+    {
+        private static IEnumerable<string> ParamKeywords { get; } = ["me", "all"];
+
+        private static string ParamKeywordsToString { get; } = "<me, all | blank, uuid>";
+
+        /// <summary>
+        /// Checks if any packages exist with given ID.
+        /// </summary>
+        /// <param name="packages">Lists of packages</param>
+        /// <param name="paramName">name of query parameter</param>
+        /// <returns></returns>
+        public static FuncExpression AnyPackages(IEnumerable<ConnectionPackage> packages, string paramName = "packageId") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.Any())
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.InvalidResource, $"QUERY/{paramName}", [new("Packages", $"No packages found.")]);
+        };
+
+        /// <summary>
+        /// Checks the list of packages all <see cref="Package.IsAssignable"/> is set to true.
+        /// </summary>
+        /// <param name="packages">list of packages</param>
+        /// <param name="paramName">name of the query parameter</param>
+        /// <returns></returns>
+        public static FuncExpression PackageIsAssignableByDefinition(IEnumerable<ExtConnectionPackage> packages, string paramName = "packageId") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.All(t => t.Package.IsAssignable))
+            {
+                return null;
+            }
+
+            var packagesNotAssignable = packages
+                .Where(p => p.Package.IsAssignable)
+                .Select(p => p.Id);
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/{paramName}", [new("Packages", $"{string.Join(",", packagesNotAssignable)} are not assignable.")]);
+        };
+
+        /// <summary>
+        /// Checks is packages can be asigned by checking <see cref="ConnectionPackage.CanAssign"/> is set to to true.
+        /// </summary>
+        /// <param name="packages">List of packages.</param>
+        /// <param name="paramName">name of the query parameter.</param>
+        /// <returns></returns>
+        public static FuncExpression PackageIsAssignableByUser(IEnumerable<ConnectionPackage> packages, string paramName = "packageId") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.All(t => t.CanAssign))
+            {
+                return null;
+            }
+
+            var packagesNotAssignableByUser = packages
+                .Where(p => p.CanAssign)
+                .Select(p => p.Id);
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/{paramName}", [new("Packages", $"Packages with IDs {string.Join(",", packagesNotAssignableByUser)} can't be assigned by user.")]);
+        };
+
+        /// <summary>
+        /// Used to check if package exists by check URN and resulkt of the DB lookup.
+        /// </summary>
+        /// <param name="packages">List of packags</param>
+        /// <param name="paramName">name of the query URN parameter.</param>
+        /// <returns></returns>
+        public static FuncExpression PackageUrnLookup(IEnumerable<Package> packages, string paramName = "packageUrn") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages?.Count() == 1)
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.AssignmentIsActiveInOneOrMoreDelegations, $"QUERY/{paramName}", [new("packages", string.Join(",", packages.Select(p => p.Id.ToString())))]);
+        };
+
+        /// <summary>
+        /// Checks if any packages are assigned. Used along with cascade delete
+        /// </summary>
+        /// <param name="packages">List of packages.</param>
+        /// <param name="paramName">name of the query parameter.</param>
+        public static FuncExpression HasPackagesAssigned(IEnumerable<AssignmentPackage> packages, string paramName = "cascade") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.Any())
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.AssignmentIsActiveInOneOrMoreDelegations, $"QUERY/{paramName}", [new("packages", $"following packages has active assignments [{string.Join(",", packages.Select(p => p.Id.ToString()))}].")]);
+            }
+
+            return null;
+        };
+
+        /// <summary>
+        /// Checks if any delegations are given assigned. Used along with cascade delete
+        /// </summary>
+        /// <param name="delegations">List of delegations.</param>
+        /// <param name="paramName">name of the query parameter.</param>
+        public static FuncExpression HasDelegationsAssigned(IEnumerable<Delegation> delegations, string paramName = "cascade") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(delegations);
+            if (delegations.Any())
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.EntityNotExists, $"QUERY/{paramName}", [new("packages", string.Join(",", delegations.Select(p => p.Id.ToString())))]);
+            }
+
+            return null;
+        };
+
+        /// <summary>
+        /// Checks if party exists
+        /// </summary>
+        /// <param name="party"></param>
+        /// <param name="paramName"></param>
+        /// <returns></returns>
+        public static FuncExpression PartyExists(Entity party, string paramName = "party") => () =>
+        {
+            if (party is { })
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.EntityNotExists, $"QUERY/{paramName}", [new("party", $"Entity do not exists.")]);
+        };
+
+        public static FuncExpression PartyIsEntityType(ExtEntity party, string entityType, string paramName = "party") => () =>
+        {
+            if (party is { })
+            {
+                if (party.Type.Name.Equals(entityType, StringComparison.InvariantCultureIgnoreCase))
+                {
+                    return null;
+                }
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.EntityNotExists, $"QUERY/{paramName}", [new("party", $"Entity do not exists or entity is not of type '{entityType}'.")]);
+        };
+
+        /// <summary>
+        /// Validates combination of input parameters
+        /// </summary>
+        public static FuncExpression EnduserCreateCombination(Guid useruuid, string party, string from, string to) => () =>
+        {
+            if (!string.IsNullOrEmpty(party) && party.Equals("me", StringComparison.InvariantCultureIgnoreCase))
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/EnduserValidationRules.cs
user: login: jonkjetiloye
body: Skulle helst sett at vi holdt `me` ute av logikken. Så at `me` blir replaced på input i API-laget før man gjør validering? 

Da får vi også _en_ plass for den logikken som da også kan feature toggles
created_at: 2025-05-07T11:49:18Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,464 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using Altinn.AccessManagement.Core.Errors;
+using Altinn.AccessMgmt.Core.Models;
+using Altinn.AccessMgmt.Persistence.Repositories;
+using Altinn.AccessMgmt.Persistence.Repositories.Contracts;
+using Altinn.AccessMgmt.Persistence.Services;
+using Altinn.Authorization.ProblemDetails;
+
+namespace Altinn.AccessManagement.Enduser.Services;
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Services/ConnectionService.cs
user: login: jonkjetiloye
body: Service i API-laget altså. BFA (Backend-for-API) 🤔 
created_at: 2025-05-07T12:00:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,538 @@
+using Altinn.AccessMgmt.Core.Models;
+using Altinn.Authorization.ProblemDetails;
+
+namespace Altinn.AccessManagement.Core.Errors;
+
+/// <summary>
+/// A utility class that provides methods for validating data using a series of rules.
+/// </summary>
+public static class ValidationRules
+{
+    /// <summary>
+    /// A delegate representing a validation rule that accepts a reference to a <see cref="ValidationErrorBuilder"/>
+    /// and adds errors if validation fails.
+    /// </summary>
+    /// <param name="errors">The reference to the <see cref="ValidationErrorBuilder"/> where validation errors are added.</param>
+    public delegate void Func(ref ValidationErrorBuilder errors);
+
+    /// <summary>
+    /// A delegate that returns a validation rule that accepts a reference to a <see cref="ValidationErrorBuilder"/>
+    /// and adds errors if validation fails.
+    /// </summary>
+    public delegate Func FuncExpression();
+
+    /// <summary>
+    /// Validates a series of rules against the provided parameters.
+    /// It executes each rule in sequence and returns a <see cref="ValidationProblemInstance"/>
+    /// if any validation errors are found, or <c>null</c> if no errors exist.
+    /// </summary>
+    /// <param name="rules">An array of validation rules to apply.</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> containing validation errors, if any,
+    /// or <c>null</c> if the validation passed without any errors.
+    /// </returns>
+    public static ValidationProblemInstance? Validate(params FuncExpression[] rules)
+    {
+        var builder = default(ValidationErrorBuilder);
+        All(rules)()(ref builder);
+        builder.TryBuild(out var result);
+        return result;
+    }
+
+    /// <summary>
+    /// Validates a series of rules against the provided parameters.
+    /// It executes each rule in sequence and returns a <see cref="ValidationProblemInstance"/>
+    /// if any validation errors are found, or <c>null</c> if no errors exist.
+    /// </summary>
+    /// <param name="rules">An array of validation rules to apply.</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> containing validation errors, if any,
+    /// or <c>null</c> if the validation passed without any errors.
+    /// </returns>
+    public static ValidationProblemInstance? Validate(params Func[] rules)
+    {
+        var builder = default(ValidationErrorBuilder);
+        foreach (var rule in rules)
+        {
+            rule(ref builder);
+        }
+
+        builder.TryBuild(out var result);
+
+        return result;
+    }
+
+    /// <summary>
+    /// Combines multiple validation rules that must all pass.
+    /// </summary>
+    /// <param name="funcs">The validation functions to combine.</param>
+    /// <returns>A combined validation rule that applies all the specified rules.</returns>
+    public static FuncExpression All(params FuncExpression[] funcs) => () =>
+    {
+        var results = new List<Func>();
+        foreach (var func in funcs)
+        {
+            if (func() is var fn && fn is { })
+            {
+                results.Add(fn);
+            }
+        }
+
+        return (ref ValidationErrorBuilder errors) =>
+        {
+            foreach (var result in results)
+            {
+                result(ref errors);
+            }
+        };
+    };
+
+    /// <summary>
+    /// Combines multiple validation rules where at least one must pass.
+    /// </summary>
+    /// <param name="funcs">The validation functions to combine.</param>
+    /// <returns>A combined validation rule that applies any of the specified rules.</returns>
+    public static FuncExpression Any(params FuncExpression[] funcs) => () =>
+    {
+        var results = new List<Func>();
+        foreach (var func in funcs)
+        {
+            if (func() is var fn && fn is { })
+            {
+                results.Add(fn);
+            }
+        }
+
+        if (results.Count == funcs.Length)
+        {
+            return (ref ValidationErrorBuilder errors) =>
+            {
+                foreach (var result in results)
+                {
+                    result(ref errors);
+                }
+            };
+        }
+
+        return null;
+    };
+
+    /// <summary>
+    /// Validates the default query parameters for an end user.
+    /// </summary>
+    /// <param name="userUuid">The UUID of the user performing the action (current user).</param>
+    /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+    /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+    /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> with validation errors if any.
+    /// </returns>
+    public static ValidationProblemInstance EnduserAddConnection(Guid userUuid, string party, string from, string to) => Validate(
+        QueryParameters.Party(party),
+        Any(
+            QueryParameters.PartyFrom(from),
+            QueryParameters.PartyTo(to)
+        ),
+        QueryParameters.EnduserCreateCombination(userUuid, party, from, to)
+    );
+
+    /// <summary>
+    /// Validates the default query parameters for an end user.
+    /// </summary>
+    /// <param name="userUuid">The UUID of the user performing the action (current user).</param>
+    /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+    /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+    /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> with validation errors if any.
+    /// </returns>
+    public static ValidationProblemInstance EnduserGetConnection(Guid userUuid, string party, string from, string to) => Validate(
+        QueryParameters.Party(party),
+        QueryParameters.EnduserReadInputCombination(userUuid, party, from, to),
+        Any(
+            QueryParameters.PartyFrom(from),
+            QueryParameters.PartyTo(to)
+        )
+    );
+
+    /// <summary>
+    /// Validates the default query parameters for an end user.
+    /// </summary>
+    /// <param name="userUuid">The UUID of the user performing the action (current user).</param>
+    /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+    /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+    /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> with validation errors if any.
+    /// </returns>
+    public static ValidationProblemInstance EnduserRemoveConnection(Guid userUuid, string party, string from, string to) => Validate(
+        QueryParameters.Party(party),
+        Any(
+            QueryParameters.PartyFrom(from),
+            QueryParameters.PartyTo(to)
+        ),
+        QueryParameters.EnduserDeleteCombination(userUuid, party, from, to)
+    );
+
+    /// <summary>
+    /// Provides validation rules for query parameters.
+    /// </summary>
+    public static class QueryParameters
+    {
+        private static IEnumerable<string> ParamKeywords { get; } = ["me", "all"];
+
+        private static string ParamKeywordsToString { get; } = "<me, all | blank, uuid>";
+
+        /// <summary>
+        /// Checks if any packages exist with given ID.
+        /// </summary>
+        /// <param name="packages">Lists of packages</param>
+        /// <param name="paramName">name of query parameter</param>
+        /// <returns></returns>
+        public static FuncExpression AnyPackages(IEnumerable<ConnectionPackage> packages, string paramName = "packageId") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.Any())
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.InvalidResource, $"QUERY/{paramName}", [new("Packages", $"No packages found.")]);
+        };
+
+        /// <summary>
+        /// Checks the list of packages all <see cref="Package.IsAssignable"/> is set to true.
+        /// </summary>
+        /// <param name="packages">list of packages</param>
+        /// <param name="paramName">name of the query parameter</param>
+        /// <returns></returns>
+        public static FuncExpression PackageIsAssignableByDefinition(IEnumerable<ExtConnectionPackage> packages, string paramName = "packageId") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.All(t => t.Package.IsAssignable))
+            {
+                return null;
+            }
+
+            var packagesNotAssignable = packages
+                .Where(p => p.Package.IsAssignable)
+                .Select(p => p.Id);
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/{paramName}", [new("Packages", $"{string.Join(",", packagesNotAssignable)} are not assignable.")]);
+        };
+
+        /// <summary>
+        /// Checks is packages can be asigned by checking <see cref="ConnectionPackage.CanAssign"/> is set to to true.
+        /// </summary>
+        /// <param name="packages">List of packages.</param>
+        /// <param name="paramName">name of the query parameter.</param>
+        /// <returns></returns>
+        public static FuncExpression PackageIsAssignableByUser(IEnumerable<ConnectionPackage> packages, string paramName = "packageId") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.All(t => t.CanAssign))
+            {
+                return null;
+            }
+
+            var packagesNotAssignableByUser = packages
+                .Where(p => p.CanAssign)
+                .Select(p => p.Id);
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/{paramName}", [new("Packages", $"Packages with IDs {string.Join(",", packagesNotAssignableByUser)} can't be assigned by user.")]);
+        };
+
+        /// <summary>
+        /// Used to check if package exists by check URN and resulkt of the DB lookup.
+        /// </summary>
+        /// <param name="packages">List of packags</param>
+        /// <param name="paramName">name of the query URN parameter.</param>
+        /// <returns></returns>
+        public static FuncExpression PackageUrnLookup(IEnumerable<Package> packages, string paramName = "packageUrn") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages?.Count() == 1)
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.AssignmentIsActiveInOneOrMoreDelegations, $"QUERY/{paramName}", [new("packages", string.Join(",", packages.Select(p => p.Id.ToString())))]);
+        };
+
+        /// <summary>
+        /// Checks if any packages are assigned. Used along with cascade delete
+        /// </summary>
+        /// <param name="packages">List of packages.</param>
+        /// <param name="paramName">name of the query parameter.</param>
+        public static FuncExpression HasPackagesAssigned(IEnumerable<AssignmentPackage> packages, string paramName = "cascade") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.Any())
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.AssignmentIsActiveInOneOrMoreDelegations, $"QUERY/{paramName}", [new("packages", $"following packages has active assignments [{string.Join(",", packages.Select(p => p.Id.ToString()))}].")]);
+            }
+
+            return null;
+        };
+
+        /// <summary>
+        /// Checks if any delegations are given assigned. Used along with cascade delete
+        /// </summary>
+        /// <param name="delegations">List of delegations.</param>
+        /// <param name="paramName">name of the query parameter.</param>
+        public static FuncExpression HasDelegationsAssigned(IEnumerable<Delegation> delegations, string paramName = "cascade") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(delegations);
+            if (delegations.Any())
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.EntityNotExists, $"QUERY/{paramName}", [new("packages", string.Join(",", delegations.Select(p => p.Id.ToString())))]);
+            }
+
+            return null;
+        };
+
+        /// <summary>
+        /// Checks if party exists
+        /// </summary>
+        /// <param name="party"></param>
+        /// <param name="paramName"></param>
+        /// <returns></returns>
+        public static FuncExpression PartyExists(Entity party, string paramName = "party") => () =>
+        {
+            if (party is { })
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.EntityNotExists, $"QUERY/{paramName}", [new("party", $"Entity do not exists.")]);
+        };
+
+        public static FuncExpression PartyIsEntityType(ExtEntity party, string entityType, string paramName = "party") => () =>
+        {
+            if (party is { })
+            {
+                if (party.Type.Name.Equals(entityType, StringComparison.InvariantCultureIgnoreCase))
+                {
+                    return null;
+                }
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.EntityNotExists, $"QUERY/{paramName}", [new("party", $"Entity do not exists or entity is not of type '{entityType}'.")]);
+        };
+
+        /// <summary>
+        /// Validates combination of input parameters
+        /// </summary>
+        public static FuncExpression EnduserCreateCombination(Guid useruuid, string party, string from, string to) => () =>
+        {
+            if (!string.IsNullOrEmpty(party) && party.Equals("me", StringComparison.InvariantCultureIgnoreCase))
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/EnduserValidationRules.cs
user: login: andreasisnes
body: true, tenker du en middleware / filter som replacer alle me verdier?
created_at: 2025-05-07T12:06:15Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,464 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using Altinn.AccessManagement.Core.Errors;
+using Altinn.AccessMgmt.Core.Models;
+using Altinn.AccessMgmt.Persistence.Repositories;
+using Altinn.AccessMgmt.Persistence.Repositories.Contracts;
+using Altinn.AccessMgmt.Persistence.Services;
+using Altinn.Authorization.ProblemDetails;
+
+namespace Altinn.AccessManagement.Enduser.Services;
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Services/ConnectionService.cs
user: login: andreasisnes
body: okey, så me i access mgmt APIet skal ikke forhode seg til me?

created_at: 2025-05-07T12:06:44Z
author_association: COLLABORATOR
diff_hunk: @@ -48,7 +48,7 @@ public async Task<ActionResult> GetAccessParties()
     /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpPost]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
+    [ServiceFilter(typeof(SetEnduserAudit))]
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/AccessPartiesController.cs
user: login: jonkjetiloye
body: Denne controlleren kan vel slettes gitt at endepunktene er erstattet med ConnectionsController
created_at: 2025-05-09T09:22:11Z
author_association: MEMBER
diff_hunk: @@ -48,7 +48,7 @@ public async Task<ActionResult> GetAccessParties()
     /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpPost]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
+    [ServiceFilter(typeof(SetEnduserAudit))]
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/AccessPartiesController.cs
user: login: andreasisnes
body: jepp, det kan den
created_at: 2025-05-09T09:45:58Z
author_association: COLLABORATOR
diff_hunk: @@ -56,6 +66,14 @@ public static IHostApplicationBuilder AddAccessMgmtDb(this IHostApplicationBuild
             }
         }
 
+        builder.Services.AddSingleton<AuditMiddleware>();
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Persistence/Extensions/DbAccessHostExtensions.cs
user: login: Copilot
body: Duplicate registrations of AuditMiddleware, IDbAudit, and IDbAuditService are present. Remove the duplicates to prevent unintended behavior.
created_at: 2025-05-09T09:53:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,17 +9,17 @@ namespace Altinn.AccessManagement.Core.Filters;
 /// Verifies that user uuid claim exists in token and adds the value to http context items bag.
 /// If claim is not present it will return cancel pipeline and return 401 unauthorized.
 /// </summary>
-public partial class AuthorizePartyUuidClaimFilter(ILogger<AuthorizePartyUuidClaimFilter> logger) : IAuthorizationFilter
+public partial class SetEnduserAudit(ILogger<SetEnduserAudit> logger) : IAuthorizationFilter
 {
-    private ILogger<AuthorizePartyUuidClaimFilter> Logger { get; } = logger;
+    private ILogger<SetEnduserAudit> Logger { get; } = logger;
 
     /// <inheritdoc/>
     public void OnAuthorization(AuthorizationFilterContext context)
     {
         if (context.HttpContext.User?.Identity?.IsAuthenticated != true)
         {
             Log.ThereIsNoAuthorizedUser(Logger);
-            context.Result = new UnauthorizedResult();
+            // context.Result = new UnauthorizedResult();
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Core/Filters/AuthorizePartyUuidClaimFilter.cs
user: login: Copilot
body: The unauthorized handling code is commented out in SetEnduserAudit, which may allow unauthenticated requests to proceed unexpectedly. Consider restoring the unauthorized result to maintain security.
```suggestion
            context.Result = new UnauthorizedResult();
```
created_at: 2025-05-09T09:53:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,281 +22,184 @@ namespace Altinn.AccessManagement.Api.Enduser.Controllers;
 [Route("accessmanagement/api/v1/enduser/connections")]
 [FeatureGate(AccessManagementEnduserFeatureFlags.ControllerConnections)]
 [Authorize(Policy = AuthzConstants.SCOPE_PORTAL_ENDUSER)]
-public class ConnectionController(IHttpContextAccessor accessor, IConnectionService connectionService, IAssignmentService assignmentService, IEntityRepository entityRepository) : ControllerBase
+public class ConnectionController(IEnduserConnectionService connectionService) : ControllerBase
 {
-    private IHttpContextAccessor Accessor { get; } = accessor;
+    private IEnduserConnectionService ConnectionService { get; } = connectionService;
 
     /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
+    /// Get connections between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpGet]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<AssignmentExternal>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetConnections([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
-        {
-            return BadRequest();
-        }
-
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
-        {
-            // Party must match From or To
-            return BadRequest();
-        }
-
-        var partyUuid = Accessor.GetPartyUuid();
-        var audit = new ChangeRequestOptions()
-        {
-            ChangedBy = partyUuid,
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from.HasValue && to.HasValue)
+        if (ValidationRules.EnduserReadConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Ok(await connectionService.Get(fromId: from.Value, toId: to.Value, facilitatorId: null, cancellationToken: cancellationToken));
+            return problem.ToActionResult();
         }
 
-        if (from.HasValue)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        var result = await ConnectionService.Get(fromUuid, toUuid, cancellationToken: cancellationToken);
+        if (result.IsProblem)
         {
-            return Ok(await connectionService.GetReceived(from.Value, cancellationToken: cancellationToken));
+            return result.Problem.ToActionResult();
         }
 
-        if (to.HasValue)
-        {
-            return Ok(await connectionService.GetGiven(to.Value, cancellationToken: cancellationToken));
-        }
-        
-        return BadRequest();
+        return Ok(PaginatedResult.Create(result.Value, null));
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
     [HttpPost]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, CancellationToken cancellationToken = default)
+    [ProducesResponseType<Assignment>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddAssignment([FromQuery] ConnectionInput connection, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from != party)
-        {
-            throw new Exception("From party does not match from");
-        }
-
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
-        {
-            return Problem("From party not found");
-        }
-
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("From must be of type 'Organisasjon'");
-        }
-
-        //// To must be Type:Organisasjon
-        //// #550:AC:Det skal bare være mulig å legge til Organisasjoner som ny Rightholder
-        var toEntity = await entityRepository.GetExtended(to);
-        if (toEntity == null)
+        if (ValidationRules.EnduserAddConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Problem("To party not found");
+            return problem.ToActionResult();
         }
 
-        if (!toEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        var result = await ConnectionService.AddAssignment(fromUuid, toUuid, "rettighetshaver", cancellationToken);
+        if (result.IsProblem)
         {
-            return Problem("To must be of type 'Organisasjon'");
+            return result.Problem.ToActionResult();
         }
 
-        var res = await assignmentService.GetOrCreateAssignmentInternal(fromId: from, toId: to, roleCode: "rettighetshaver", options, cancellationToken: cancellationToken);
-
-        if (res != null)
-        {
-            return Ok(res);
-        }
-
-        return Problem("Unable add connection");
+        return Ok(result.Value);
     }
 
     /// <summary>
     /// Remove package from connection (assignment or delegation)
     /// </summary>
     [HttpDelete]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<ProblemInstance> RemoveConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> RemoveAssignment([FromQuery] ConnectionInput connection, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (!(from == party) && !(to == party))
+        if (ValidationRules.EnduserRemoveConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            throw new Exception("From party does not match from or to");
+            return problem.ToActionResult();
         }
 
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        problem = await ConnectionService.RemoveAssignment(fromUuid, toUuid, "rettighetshaver", cascade, cancellationToken);
+        if (problem is { })
         {
-            throw new Exception("From party not found");
+            problem.ToActionResult();
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
user: login: Copilot
body: Missing return statement in the error handling block; ensure that the result of problem.ToActionResult() is returned to propagate the error response correctly.
```suggestion
            return problem.ToActionResult();
```
created_at: 2025-05-09T09:53:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,281 +22,184 @@ namespace Altinn.AccessManagement.Api.Enduser.Controllers;
 [Route("accessmanagement/api/v1/enduser/connections")]
 [FeatureGate(AccessManagementEnduserFeatureFlags.ControllerConnections)]
 [Authorize(Policy = AuthzConstants.SCOPE_PORTAL_ENDUSER)]
-public class ConnectionController(IHttpContextAccessor accessor, IConnectionService connectionService, IAssignmentService assignmentService, IEntityRepository entityRepository) : ControllerBase
+public class ConnectionController(IEnduserConnectionService connectionService) : ControllerBase
 {
-    private IHttpContextAccessor Accessor { get; } = accessor;
+    private IEnduserConnectionService ConnectionService { get; } = connectionService;
 
     /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
+    /// Get connections between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpGet]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<AssignmentExternal>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetConnections([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
-        {
-            return BadRequest();
-        }
-
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
-        {
-            // Party must match From or To
-            return BadRequest();
-        }
-
-        var partyUuid = Accessor.GetPartyUuid();
-        var audit = new ChangeRequestOptions()
-        {
-            ChangedBy = partyUuid,
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from.HasValue && to.HasValue)
+        if (ValidationRules.EnduserReadConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Ok(await connectionService.Get(fromId: from.Value, toId: to.Value, facilitatorId: null, cancellationToken: cancellationToken));
+            return problem.ToActionResult();
         }
 
-        if (from.HasValue)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        var result = await ConnectionService.Get(fromUuid, toUuid, cancellationToken: cancellationToken);
+        if (result.IsProblem)
         {
-            return Ok(await connectionService.GetReceived(from.Value, cancellationToken: cancellationToken));
+            return result.Problem.ToActionResult();
         }
 
-        if (to.HasValue)
-        {
-            return Ok(await connectionService.GetGiven(to.Value, cancellationToken: cancellationToken));
-        }
-        
-        return BadRequest();
+        return Ok(PaginatedResult.Create(result.Value, null));
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
     [HttpPost]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, CancellationToken cancellationToken = default)
+    [ProducesResponseType<Assignment>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddAssignment([FromQuery] ConnectionInput connection, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from != party)
-        {
-            throw new Exception("From party does not match from");
-        }
-
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
-        {
-            return Problem("From party not found");
-        }
-
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("From must be of type 'Organisasjon'");
-        }
-
-        //// To must be Type:Organisasjon
-        //// #550:AC:Det skal bare være mulig å legge til Organisasjoner som ny Rightholder
-        var toEntity = await entityRepository.GetExtended(to);
-        if (toEntity == null)
+        if (ValidationRules.EnduserAddConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Problem("To party not found");
+            return problem.ToActionResult();
         }
 
-        if (!toEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        var result = await ConnectionService.AddAssignment(fromUuid, toUuid, "rettighetshaver", cancellationToken);
+        if (result.IsProblem)
         {
-            return Problem("To must be of type 'Organisasjon'");
+            return result.Problem.ToActionResult();
         }
 
-        var res = await assignmentService.GetOrCreateAssignmentInternal(fromId: from, toId: to, roleCode: "rettighetshaver", options, cancellationToken: cancellationToken);
-
-        if (res != null)
-        {
-            return Ok(res);
-        }
-
-        return Problem("Unable add connection");
+        return Ok(result.Value);
     }
 
     /// <summary>
     /// Remove package from connection (assignment or delegation)
     /// </summary>
     [HttpDelete]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<ProblemInstance> RemoveConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> RemoveAssignment([FromQuery] ConnectionInput connection, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (!(from == party) && !(to == party))
+        if (ValidationRules.EnduserRemoveConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            throw new Exception("From party does not match from or to");
+            return problem.ToActionResult();
         }
 
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        problem = await ConnectionService.RemoveAssignment(fromUuid, toUuid, "rettighetshaver", cascade, cancellationToken);
+        if (problem is { })
         {
-            throw new Exception("From party not found");
+            problem.ToActionResult();
         }
 
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            throw new Exception("From must be of type 'Organisasjon'");
-        }
-
-        return await assignmentService.DeleteAssignment(fromId: from, toId: to, roleCode: "rettighetshaver", options, cascade: cascade, cancellationToken);
+        return NoContent();
     }
 
     /// <summary>
     /// Creates an assignment between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
-    [HttpGet]
-    [Route("accesspackages")]
+    [HttpGet("accesspackages")]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [ProducesResponseType<PaginatedResult<ConnectionPackage>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetPackages([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetPackages([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
+        if (ValidationRules.EnduserReadConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        var result = await ConnectionService.GetPackages(fromUuid, toUuid, cancellationToken);
+        if (result.IsProblem)
         {
-            // Party must match From or To
-            return BadRequest();
+            return result.Problem.ToActionResult();
         }
 
-        var res = await connectionService.GetPackages(fromId: from, toId: to);
-
-        return Ok(res);
+        return Ok(PaginatedResult.Create(result.Value, null));
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
-    [HttpPost]
-    [Route("accesspackages")]
+    [HttpPost("accesspackages")]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddPackages([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] Guid? package, [FromQuery] string packageUrn, CancellationToken cancellationToken = default)
+    [ProducesResponseType<AssignmentPackage>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddPackages([FromQuery] ConnectionInput connection, [FromQuery] Guid? packageId, [FromQuery] string packageUrn, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from != party)
-        {
-            // Party must match From or To
-            return BadRequest();
-        }
-
-        //// From must by Type:Organisasjon
-        //// #568:AC:From party må være en Organisasjon (skal ikke være mulig å delegere fra privatperson el. andre entitetstyper enda)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
-        {
-            throw new Exception("From party not found");
-        }
-
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
+        if (ValidationRules.EnduserAddConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            throw new Exception("From must be of type 'Organisasjon'");
+            return problem.ToActionResult();
         }
 
-        //// To must be Type:Organisasjon
-        //// #568:AC:To party må være en Organisasjon (skal ikke være mulig å delegere fra privatperson el. andre entitetstyper enda)
-        var toEntity = await entityRepository.GetExtended(to);
-        if (toEntity == null)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        async Task<Result<AssignmentPackage>> AddPackage()
         {
-            return Problem("To party not found");
-        }
-
-        if (!toEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("To must be of type 'Organisasjon'");
-        }
-
-        if (package.HasValue)
-        {
-            var res = await connectionService.AddPackage(fromId: from, toId: to, roleCode: "rettighetshaver", packageId: package.Value, options);
-            if (res)
+            if (packageId.HasValue)
             {
-                return Ok();
+                return await ConnectionService.AddPackage(fromUuid, toUuid, "rettighetshaver", packageId.Value, cancellationToken);
             }
+
+            return await ConnectionService.AddPackage(fromUuid, toUuid, "rettighetshaver", packageUrn, cancellationToken);
         }
-        else
+
+        var result = await AddPackage();
+        if (result.IsProblem)
         {
-            packageUrn = packageUrn.ToLower().StartsWith("urn:") ? packageUrn : ":" + packageUrn;
-            var res = await connectionService.AddPackage(fromId: from, toId: to, roleCode: "rettighetshaver", packageUrn: packageUrn, options);
-            if (res)
-            {
-                return Ok();
-            }
+            result.Problem.ToActionResult();
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
user: login: Copilot
body: The error response from result.Problem.ToActionResult() is not returned, which may cause the method to continue executing. Add a return statement to properly return the error response.
```suggestion
            return result.Problem.ToActionResult();
```
created_at: 2025-05-09T09:53:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,281 +22,184 @@ namespace Altinn.AccessManagement.Api.Enduser.Controllers;
 [Route("accessmanagement/api/v1/enduser/connections")]
 [FeatureGate(AccessManagementEnduserFeatureFlags.ControllerConnections)]
 [Authorize(Policy = AuthzConstants.SCOPE_PORTAL_ENDUSER)]
-public class ConnectionController(IHttpContextAccessor accessor, IConnectionService connectionService, IAssignmentService assignmentService, IEntityRepository entityRepository) : ControllerBase
+public class ConnectionController(IEnduserConnectionService connectionService) : ControllerBase
 {
-    private IHttpContextAccessor Accessor { get; } = accessor;
+    private IEnduserConnectionService ConnectionService { get; } = connectionService;
 
     /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
+    /// Get connections between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpGet]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<AssignmentExternal>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetConnections([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
-        {
-            return BadRequest();
-        }
-
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
-        {
-            // Party must match From or To
-            return BadRequest();
-        }
-
-        var partyUuid = Accessor.GetPartyUuid();
-        var audit = new ChangeRequestOptions()
-        {
-            ChangedBy = partyUuid,
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from.HasValue && to.HasValue)
+        if (ValidationRules.EnduserReadConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Ok(await connectionService.Get(fromId: from.Value, toId: to.Value, facilitatorId: null, cancellationToken: cancellationToken));
+            return problem.ToActionResult();
         }
 
-        if (from.HasValue)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        var result = await ConnectionService.Get(fromUuid, toUuid, cancellationToken: cancellationToken);
+        if (result.IsProblem)
         {
-            return Ok(await connectionService.GetReceived(from.Value, cancellationToken: cancellationToken));
+            return result.Problem.ToActionResult();
         }
 
-        if (to.HasValue)
-        {
-            return Ok(await connectionService.GetGiven(to.Value, cancellationToken: cancellationToken));
-        }
-        
-        return BadRequest();
+        return Ok(PaginatedResult.Create(result.Value, null));
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
     [HttpPost]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, CancellationToken cancellationToken = default)
+    [ProducesResponseType<Assignment>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddAssignment([FromQuery] ConnectionInput connection, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from != party)
-        {
-            throw new Exception("From party does not match from");
-        }
-
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
-        {
-            return Problem("From party not found");
-        }
-
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("From must be of type 'Organisasjon'");
-        }
-
-        //// To must be Type:Organisasjon
-        //// #550:AC:Det skal bare være mulig å legge til Organisasjoner som ny Rightholder
-        var toEntity = await entityRepository.GetExtended(to);
-        if (toEntity == null)
+        if (ValidationRules.EnduserAddConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Problem("To party not found");
+            return problem.ToActionResult();
         }
 
-        if (!toEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        var result = await ConnectionService.AddAssignment(fromUuid, toUuid, "rettighetshaver", cancellationToken);
+        if (result.IsProblem)
         {
-            return Problem("To must be of type 'Organisasjon'");
+            return result.Problem.ToActionResult();
         }
 
-        var res = await assignmentService.GetOrCreateAssignmentInternal(fromId: from, toId: to, roleCode: "rettighetshaver", options, cancellationToken: cancellationToken);
-
-        if (res != null)
-        {
-            return Ok(res);
-        }
-
-        return Problem("Unable add connection");
+        return Ok(result.Value);
     }
 
     /// <summary>
     /// Remove package from connection (assignment or delegation)
     /// </summary>
     [HttpDelete]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<ProblemInstance> RemoveConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> RemoveAssignment([FromQuery] ConnectionInput connection, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (!(from == party) && !(to == party))
+        if (ValidationRules.EnduserRemoveConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            throw new Exception("From party does not match from or to");
+            return problem.ToActionResult();
         }
 
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        problem = await ConnectionService.RemoveAssignment(fromUuid, toUuid, "rettighetshaver", cascade, cancellationToken);
+        if (problem is { })
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
user: login: jonkjetiloye
body: er denne sjekken reversert? sjekker ikke denne om problem nå er ett tomt objekt som vel er positivt resultat ikke negativt?
created_at: 2025-05-09T09:57:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,530 @@
+using Altinn.AccessMgmt.Core.Models;
+using Altinn.Authorization.ProblemDetails;
+
+namespace Altinn.AccessManagement.Core.Errors;
+
+/// <summary>
+/// A utility class that provides methods for validating data using a series of rules.
+/// </summary>
+public static class ValidationRules
+{
+    /// <summary>
+    /// A delegate representing a validation rule that accepts a reference to a <see cref="ValidationErrorBuilder"/>
+    /// and adds errors if validation fails.
+    /// </summary>
+    /// <param name="errors">The reference to the <see cref="ValidationErrorBuilder"/> where validation errors are added.</param>
+    public delegate void Func(ref ValidationErrorBuilder errors);
+
+    /// <summary>
+    /// A delegate that returns a validation rule that accepts a reference to a <see cref="ValidationErrorBuilder"/>
+    /// and adds errors if validation fails.
+    /// </summary>
+    public delegate Func FuncExpression();
+
+    /// <summary>
+    /// Validates a series of rules against the provided parameters.
+    /// It executes each rule in sequence and returns a <see cref="ValidationProblemInstance"/>
+    /// if any validation errors are found, or <c>null</c> if no errors exist.
+    /// </summary>
+    /// <param name="rules">An array of validation rules to apply.</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> containing validation errors, if any,
+    /// or <c>null</c> if the validation passed without any errors.
+    /// </returns>
+    public static ValidationProblemInstance? Validate(params FuncExpression[] rules)
+    {
+        var builder = default(ValidationErrorBuilder);
+        All(rules)()(ref builder);
+        builder.TryBuild(out var result);
+        return result;
+    }
+
+    /// <summary>
+    /// Validates a series of rules against the provided parameters.
+    /// It executes each rule in sequence and returns a <see cref="ValidationProblemInstance"/>
+    /// if any validation errors are found, or <c>null</c> if no errors exist.
+    /// </summary>
+    /// <param name="rules">An array of validation rules to apply.</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> containing validation errors, if any,
+    /// or <c>null</c> if the validation passed without any errors.
+    /// </returns>
+    public static ValidationProblemInstance? Validate(params Func[] rules)
+    {
+        var builder = default(ValidationErrorBuilder);
+        foreach (var rule in rules)
+        {
+            rule(ref builder);
+        }
+
+        builder.TryBuild(out var result);
+
+        return result;
+    }
+
+    /// <summary>
+    /// Combines multiple validation rules that must all pass.
+    /// </summary>
+    /// <param name="funcs">The validation functions to combine.</param>
+    /// <returns>A combined validation rule that applies all the specified rules.</returns>
+    public static FuncExpression All(params FuncExpression[] funcs) => () =>
+    {
+        var results = new List<Func>();
+        foreach (var func in funcs)
+        {
+            if (func() is var fn && fn is { })
+            {
+                results.Add(fn);
+            }
+        }
+
+        return (ref ValidationErrorBuilder errors) =>
+        {
+            foreach (var result in results)
+            {
+                result(ref errors);
+            }
+        };
+    };
+
+    /// <summary>
+    /// Combines multiple validation rules where at least one must pass.
+    /// </summary>
+    /// <param name="funcs">The validation functions to combine.</param>
+    /// <returns>A combined validation rule that applies any of the specified rules.</returns>
+    public static FuncExpression Any(params FuncExpression[] funcs) => () =>
+    {
+        var results = new List<Func>();
+        foreach (var func in funcs)
+        {
+            if (func() is var fn && fn is { })
+            {
+                results.Add(fn);
+            }
+        }
+
+        if (results.Count == funcs.Length)
+        {
+            return (ref ValidationErrorBuilder errors) =>
+            {
+                foreach (var result in results)
+                {
+                    result(ref errors);
+                }
+            };
+        }
+
+        return null;
+    };
+
+    /// <summary>
+    /// Validates the default query parameters for an end user.
+    /// </summary>
+    /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+    /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+    /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> with validation errors if any.
+    /// </returns>
+    public static ValidationProblemInstance EnduserAddConnection(string party, string from, string to) => Validate(
+        QueryParameters.Party(party),
+        QueryParameters.PartyFrom(from),
+        QueryParameters.PartyTo(to),
+        QueryParameters.EnduserAddCombination(party, from, to)
+    );
+
+    /// <summary>
+    /// Validates the default query parameters for an end user.
+    /// </summary>
+    /// <param name="userUuid">The UUID of the user performing the action (current user).</param>
+    /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+    /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+    /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> with validation errors if any.
+    /// </returns>
+    public static ValidationProblemInstance EnduserReadConnection(string party, string from, string to) => Validate(
+        QueryParameters.Party(party),
+        QueryParameters.EnduserReadInputCombination(party, from, to),
+        Any(
+            QueryParameters.PartyFrom(from),
+            QueryParameters.PartyTo(to)
+        )
+    );
+
+    /// <summary>
+    /// Validates the default query parameters for an end user.
+    /// </summary>
+    /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+    /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+    /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> with validation errors if any.
+    /// </returns>
+    public static ValidationProblemInstance EnduserRemoveConnection(string party, string from, string to) => Validate(
+        QueryParameters.Party(party),
+        QueryParameters.PartyFrom(from),
+        QueryParameters.PartyTo(to),
+        QueryParameters.EnduserRemoveCombination(party, from, to)
+    );
+
+    /// <summary>
+    /// Provides validation rules for query parameters.
+    /// </summary>
+    public static class QueryParameters
+    {
+        private static IEnumerable<string> ParamKeywords { get; } = ["me", "all"];
+
+        private static string ParamKeywordsToString { get; } = "<me, all | blank, uuid>";
+
+        /// <summary>
+        /// Checks if any packages exist with given ID.
+        /// </summary>
+        /// <param name="packages">Lists of packages</param>
+        /// <param name="paramName">name of query parameter</param>
+        /// <returns></returns>
+        public static FuncExpression AnyPackages(IEnumerable<ConnectionPackage> packages, string paramName = "packageId") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.Any())
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.InvalidResource, $"QUERY/{paramName}", [new("Packages", $"No packages found.")]);
+        };
+
+        /// <summary>
+        /// Checks the list of packages all <see cref="Package.IsAssignable"/> is set to true.
+        /// </summary>
+        /// <param name="packages">list of packages</param>
+        /// <param name="paramName">name of the query parameter</param>
+        /// <returns></returns>
+        public static FuncExpression PackageIsAssignableByDefinition(IEnumerable<ExtConnectionPackage> packages, string paramName = "packageId") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.All(t => t.Package.IsAssignable))
+            {
+                return null;
+            }
+
+            var packagesNotAssignable = packages
+                .Where(p => p.Package.IsAssignable)
+                .Select(p => p.Id);
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/{paramName}", [new("Packages", $"{string.Join(",", packagesNotAssignable)} are not assignable.")]);
+        };
+
+        /// <summary>
+        /// Checks is packages can be asigned by checking <see cref="ConnectionPackage.CanAssign"/> is set to to true.
+        /// </summary>
+        /// <param name="packages">List of packages.</param>
+        /// <param name="paramName">name of the query parameter.</param>
+        /// <returns></returns>
+        public static FuncExpression PackageIsAssignableByUser(IEnumerable<ConnectionPackage> packages, string paramName = "packageId") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.All(t => t.CanAssign))
+            {
+                return null;
+            }
+
+            var packagesNotAssignableByUser = packages
+                .Where(p => p.CanAssign)
+                .Select(p => p.Id);
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/{paramName}", [new("Packages", $"Packages with IDs {string.Join(",", packagesNotAssignableByUser)} can't be assigned by user.")]);
+        };
+
+        /// <summary>
+        /// Used to check if package exists by check URN and resulkt of the DB lookup.
+        /// </summary>
+        /// <param name="packages">List of packags</param>
+        /// <param name="paramName">name of the query URN parameter.</param>
+        /// <returns></returns>
+        public static FuncExpression PackageUrnLookup(IEnumerable<Package> packages, string paramName = "packageUrn") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages?.Count() == 1)
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.AssignmentIsActiveInOneOrMoreDelegations, $"QUERY/{paramName}", [new("packages", string.Join(",", packages.Select(p => p.Id.ToString())))]);
+        };
+
+        /// <summary>
+        /// Checks if any packages are assigned. Used along with cascade delete
+        /// </summary>
+        /// <param name="packages">List of packages.</param>
+        /// <param name="paramName">name of the query parameter.</param>
+        public static FuncExpression HasPackagesAssigned(IEnumerable<AssignmentPackage> packages, string paramName = "cascade") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.Any())
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.AssignmentIsActiveInOneOrMoreDelegations, $"QUERY/{paramName}", [new("packages", $"following packages has active assignments [{string.Join(",", packages.Select(p => p.Id.ToString()))}].")]);
+            }
+
+            return null;
+        };
+
+        /// <summary>
+        /// Checks whether any packages are assigned. Used in conjunction with cascade delete.
+        /// </summary>
+        /// <param name="packages">List of role assignments.</param>
+        /// <param name="roleCode">The name of the role to verify.</param>
+        /// <param name="paramNameFrom">The name of the source query parameter.</param>
+        /// <param name="paramNameTo">The name of the target query parameter.</param>
+        public static FuncExpression VerifyAssignmentRoleExists(IEnumerable<Assignment> packages, string roleCode, string paramNameFrom = "from", string paramNameTo = "to") => () =>
+        {
+            if (packages is { } && packages.Any())
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+            {
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/{paramNameFrom}",
+                    [new("packages", $"No active role assignments of type '{roleCode}' were found from the value in query parameter '{paramNameFrom}' to the value in query parameter '{paramNameTo}'.")]);
+            };
+        };
+
+        /// <summary>
+        /// Checks if any delegations are given assigned. Used along with cascade delete
+        /// </summary>
+        /// <param name="delegations">List of delegations.</param>
+        /// <param name="paramName">name of the query parameter.</param>
+        public static FuncExpression HasDelegationsAssigned(IEnumerable<Delegation> delegations, string paramName = "cascade") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(delegations);
+            if (delegations.Any())
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.EntityNotExists, $"QUERY/{paramName}", [new("packages", string.Join(",", delegations.Select(p => p.Id.ToString())))]);
+            }
+
+            return null;
+        };
+
+        /// <summary>
+        /// Checks if party exists
+        /// </summary>
+        /// <param name="party"></param>
+        /// <param name="paramName"></param>
+        /// <returns></returns>
+        public static FuncExpression PartyExists(Entity party, string paramName = "party") => () =>
+        {
+            if (party is { })
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.EntityNotExists, $"QUERY/{paramName}", [new("party", $"Entity do not exists.")]);
+        };
+
+        public static FuncExpression PartyIsEntityType(ExtEntity party, string entityType, string paramName = "party") => () =>
+        {
+            if (party is { })
+            {
+                if (party.Type.Name.Equals(entityType, StringComparison.InvariantCultureIgnoreCase))
+                {
+                    return null;
+                }
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.EntityNotExists, $"QUERY/{paramName}", [new("party", $"Entity do not exists or entity is not of type '{entityType}'.")]);
+        };
+
+        /// <summary>
+        /// Validates combination of input parameters
+        /// </summary>
+        public static FuncExpression EnduserAddCombination(string party, string from, string to) => () =>
+        {
+            if (!Guid.TryParse(party, out var partyUuid) || partyUuid == Guid.Empty)
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/party", [new("party", $"Paramater is not a valid a UUID.")]);
+            }
+
+            if (!Guid.TryParse(to, out var fromUuid) || fromUuid == Guid.Empty)
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/party", [new("from", $"Parameter is not a valid UUID.")]);
+            }
+
+            if (!Guid.TryParse(to, out var toUuid) || toUuid == Guid.Empty)
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/party", [new("to", $"Parameter is not a valid UUID.")]);
+            }
+
+            if (partyUuid != fromUuid)
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                {
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/from", [new("from", "must match the 'party' UUID.")]);
+                };
+            }
+
+            return null;
+        };
+
+        /// <summary>
+        /// Validates that the acting party matches either the 'from' or 'to' party,
+        /// and ensures that none of the provided UUIDs are invalid or empty. If any UUID is 
+        /// set to "me", it will be replaced by the user's UUID.
+        /// </summary>
+        /// <param name="useruuid">The UUID of the user performing the action (current user).</param>
+        /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+        /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+        /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+        /// <returns>
+        /// A validation rule that adds an error if any UUID is invalid or empty, 
+        /// or if the party does not match either the 'from' or 'to' UUID.
+        /// </returns>
+        public static FuncExpression EnduserReadInputCombination(string party, string from, string to) => () =>
+        {
+            if (!Guid.TryParse(party, out var partyUuid))
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/party", [new("party", $"The 'party' parameter is not valid.")]);
+            }
+
+            if (!string.IsNullOrEmpty(from) && Guid.TryParse(from, out var fromUuid) && fromUuid == partyUuid)
+            {
+                return null;
+            }
+
+            if (!string.IsNullOrEmpty(to) && Guid.TryParse(to, out var toUuid) && toUuid == partyUuid)
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+            {
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/from", [new("from", "Either the 'from' UUID or the 'to' UUID must match the 'party' UUID. Neither matches the 'party' UUID.")]);
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/to", [new("to", "Either the 'to' UUID or the 'from' UUID must match the 'party' UUID. Neither matches the 'party' UUID.")]);
+            };
+        };
+
+        /// <summary>
+        /// Delete combination
+        /// </summary>
+        /// <param name="useruuid"></param>
+        /// <param name="party"></param>
+        /// <param name="from"></param>
+        /// <param name="to"></param>
+        /// <returns></returns>
+        public static FuncExpression EnduserRemoveCombination(string party, string from, string to) => () =>
+        {
+            if (!Guid.TryParse(party, out var partyUuid))
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/party", [new("party", $"The 'party' parameter is not valid.")]);
+            }
+
+            if (!string.IsNullOrEmpty(from) && Guid.TryParse(from, out var fromUuid) && fromUuid == partyUuid)
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+            {
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/from", [new("from", "must match the 'party' UUID.")]);
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/EnduserValidationRules.cs
user: login: jonkjetiloye
body: Validering for sletting må kreve at alle 3: party, from og to er spesifisert + at enten party og from er lik, eller at party og to er lik
created_at: 2025-05-09T10:14:11Z
author_association: MEMBER
diff_hunk: @@ -22,281 +22,184 @@ namespace Altinn.AccessManagement.Api.Enduser.Controllers;
 [Route("accessmanagement/api/v1/enduser/connections")]
 [FeatureGate(AccessManagementEnduserFeatureFlags.ControllerConnections)]
 [Authorize(Policy = AuthzConstants.SCOPE_PORTAL_ENDUSER)]
-public class ConnectionController(IHttpContextAccessor accessor, IConnectionService connectionService, IAssignmentService assignmentService, IEntityRepository entityRepository) : ControllerBase
+public class ConnectionController(IEnduserConnectionService connectionService) : ControllerBase
 {
-    private IHttpContextAccessor Accessor { get; } = accessor;
+    private IEnduserConnectionService ConnectionService { get; } = connectionService;
 
     /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
+    /// Get connections between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpGet]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<AssignmentExternal>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetConnections([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
-        {
-            return BadRequest();
-        }
-
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
-        {
-            // Party must match From or To
-            return BadRequest();
-        }
-
-        var partyUuid = Accessor.GetPartyUuid();
-        var audit = new ChangeRequestOptions()
-        {
-            ChangedBy = partyUuid,
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from.HasValue && to.HasValue)
+        if (ValidationRules.EnduserReadConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Ok(await connectionService.Get(fromId: from.Value, toId: to.Value, facilitatorId: null, cancellationToken: cancellationToken));
+            return problem.ToActionResult();
         }
 
-        if (from.HasValue)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        var result = await ConnectionService.Get(fromUuid, toUuid, cancellationToken: cancellationToken);
+        if (result.IsProblem)
         {
-            return Ok(await connectionService.GetReceived(from.Value, cancellationToken: cancellationToken));
+            return result.Problem.ToActionResult();
         }
 
-        if (to.HasValue)
-        {
-            return Ok(await connectionService.GetGiven(to.Value, cancellationToken: cancellationToken));
-        }
-        
-        return BadRequest();
+        return Ok(PaginatedResult.Create(result.Value, null));
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
     [HttpPost]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, CancellationToken cancellationToken = default)
+    [ProducesResponseType<Assignment>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddAssignment([FromQuery] ConnectionInput connection, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from != party)
-        {
-            throw new Exception("From party does not match from");
-        }
-
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
-        {
-            return Problem("From party not found");
-        }
-
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("From must be of type 'Organisasjon'");
-        }
-
-        //// To must be Type:Organisasjon
-        //// #550:AC:Det skal bare være mulig å legge til Organisasjoner som ny Rightholder
-        var toEntity = await entityRepository.GetExtended(to);
-        if (toEntity == null)
+        if (ValidationRules.EnduserAddConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Problem("To party not found");
+            return problem.ToActionResult();
         }
 
-        if (!toEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        var result = await ConnectionService.AddAssignment(fromUuid, toUuid, "rettighetshaver", cancellationToken);
+        if (result.IsProblem)
         {
-            return Problem("To must be of type 'Organisasjon'");
+            return result.Problem.ToActionResult();
         }
 
-        var res = await assignmentService.GetOrCreateAssignmentInternal(fromId: from, toId: to, roleCode: "rettighetshaver", options, cancellationToken: cancellationToken);
-
-        if (res != null)
-        {
-            return Ok(res);
-        }
-
-        return Problem("Unable add connection");
+        return Ok(result.Value);
     }
 
     /// <summary>
     /// Remove package from connection (assignment or delegation)
     /// </summary>
     [HttpDelete]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<ProblemInstance> RemoveConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> RemoveAssignment([FromQuery] ConnectionInput connection, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (!(from == party) && !(to == party))
+        if (ValidationRules.EnduserRemoveConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            throw new Exception("From party does not match from or to");
+            return problem.ToActionResult();
         }
 
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        problem = await ConnectionService.RemoveAssignment(fromUuid, toUuid, "rettighetshaver", cascade, cancellationToken);
+        if (problem is { })
         {
-            throw new Exception("From party not found");
+            problem.ToActionResult();
         }
 
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            throw new Exception("From must be of type 'Organisasjon'");
-        }
-
-        return await assignmentService.DeleteAssignment(fromId: from, toId: to, roleCode: "rettighetshaver", options, cascade: cascade, cancellationToken);
+        return NoContent();
     }
 
     /// <summary>
     /// Creates an assignment between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
-    [HttpGet]
-    [Route("accesspackages")]
+    [HttpGet("accesspackages")]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [ProducesResponseType<PaginatedResult<ConnectionPackage>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetPackages([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetPackages([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
+        if (ValidationRules.EnduserReadConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        var result = await ConnectionService.GetPackages(fromUuid, toUuid, cancellationToken);
+        if (result.IsProblem)
         {
-            // Party must match From or To
-            return BadRequest();
+            return result.Problem.ToActionResult();
         }
 
-        var res = await connectionService.GetPackages(fromId: from, toId: to);
-
-        return Ok(res);
+        return Ok(PaginatedResult.Create(result.Value, null));
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
-    [HttpPost]
-    [Route("accesspackages")]
+    [HttpPost("accesspackages")]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddPackages([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] Guid? package, [FromQuery] string packageUrn, CancellationToken cancellationToken = default)
+    [ProducesResponseType<AssignmentPackage>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddPackages([FromQuery] ConnectionInput connection, [FromQuery] Guid? packageId, [FromQuery] string packageUrn, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from != party)
-        {
-            // Party must match From or To
-            return BadRequest();
-        }
-
-        //// From must by Type:Organisasjon
-        //// #568:AC:From party må være en Organisasjon (skal ikke være mulig å delegere fra privatperson el. andre entitetstyper enda)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
-        {
-            throw new Exception("From party not found");
-        }
-
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
+        if (ValidationRules.EnduserAddConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            throw new Exception("From must be of type 'Organisasjon'");
+            return problem.ToActionResult();
         }
 
-        //// To must be Type:Organisasjon
-        //// #568:AC:To party må være en Organisasjon (skal ikke være mulig å delegere fra privatperson el. andre entitetstyper enda)
-        var toEntity = await entityRepository.GetExtended(to);
-        if (toEntity == null)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        async Task<Result<AssignmentPackage>> AddPackage()
         {
-            return Problem("To party not found");
-        }
-
-        if (!toEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("To must be of type 'Organisasjon'");
-        }
-
-        if (package.HasValue)
-        {
-            var res = await connectionService.AddPackage(fromId: from, toId: to, roleCode: "rettighetshaver", packageId: package.Value, options);
-            if (res)
+            if (packageId.HasValue)
             {
-                return Ok();
+                return await ConnectionService.AddPackage(fromUuid, toUuid, "rettighetshaver", packageId.Value, cancellationToken);
             }
+
+            return await ConnectionService.AddPackage(fromUuid, toUuid, "rettighetshaver", packageUrn, cancellationToken);
         }
-        else
+
+        var result = await AddPackage();
+        if (result.IsProblem)
         {
-            packageUrn = packageUrn.ToLower().StartsWith("urn:") ? packageUrn : ":" + packageUrn;
-            var res = await connectionService.AddPackage(fromId: from, toId: to, roleCode: "rettighetshaver", packageUrn: packageUrn, options);
-            if (res)
-            {
-                return Ok();
-            }
+            result.Problem.ToActionResult();
         }
 
-        return Problem("Unable to remove package");
+        return Ok(result.Value);
     }
 
     /// <summary>
     /// Remove package from connection (assignment or delegation)
     /// </summary>
-    [HttpDelete]
-    [Route("accesspackages")]
+    [HttpDelete("accesspackages")]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> RemovePackages([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] Guid package, CancellationToken cancellationToken = default)
+    [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> RemovePackages([FromQuery] ConnectionInput connection, [FromQuery] Guid package, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (!(from == party) && !(to == party))
+        if (ValidationRules.EnduserAddConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
user: login: jonkjetiloye
body: Denne skal vel bruke ValidationRules.EnduserRemoveConnection ?
created_at: 2025-05-09T10:14:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,6 @@
+namespace Altinn.AccessManagement.Api.Enduser.Models;
+
+public class ConnectionExternal
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Models/ConnectionExternal.cs
user: login: jonkjetiloye
body: Vi prøver vel å gå bort fra External navngivning og i stedet bruke Dto for "eksterne" modeller i API
created_at: 2025-05-09T10:18:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,417 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using Altinn.AccessManagement.Core.Errors;
+using Altinn.AccessMgmt.Core.Models;
+using Altinn.AccessMgmt.Persistence.Core.Helpers;
+using Altinn.AccessMgmt.Persistence.Repositories.Contracts;
+using Altinn.AccessMgmt.Persistence.Services;
+using Altinn.Authorization.ProblemDetails;
+
+namespace Altinn.AccessManagement.Enduser.Services;
+
+/// <summary>
+/// Service for managing connections.
+/// </summary>
+public class ConnectionService(
+    IDbAudit dbAudit,
+    IConnectionRepository connectionRepository,
+    IConnectionPackageRepository connectionPackageRepository,
+    IPackageRepository packageRepository,
+    IRoleRepository roleRepository,
+    IAssignmentRepository assignmentRepository,
+    IDelegationRepository delegationRepository,
+    IAssignmentPackageRepository assignmentPackageRepository,
+    IEntityRepository entityRepository
+    ) : IEnduserConnectionService
+{
+    private IDbAudit DbAudit { get; } = dbAudit;
+
+    private IConnectionRepository ConnectionRepository { get; } = connectionRepository;
+
+    private IConnectionPackageRepository ConnectionPackageRepository { get; } = connectionPackageRepository;
+
+    private IPackageRepository PackageRepository { get; } = packageRepository;
+
+    private IRoleRepository RoleRepository { get; } = roleRepository;
+
+    private IAssignmentRepository AssignmentRepository { get; } = assignmentRepository;
+
+    private IDelegationRepository DelegationRepository { get; } = delegationRepository;
+
+    private IAssignmentPackageRepository AssignmentPackageRepository { get; } = assignmentPackageRepository;
+
+    private IEntityRepository EntityRepository { get; } = entityRepository;
+
+    /// <inheritdoc />
+    public async Task<Result<List<ExtConnection>>> Get(Guid? from = null, Guid? to = null, CancellationToken cancellationToken = default)
+    {
+        var filter = ConnectionRepository.CreateFilterBuilder();
+        static bool SetFrom(Guid? from, GenericFilterBuilder<Connection> filter)
+        {
+            if (from != null && from.HasValue && from.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.FromId, from);
+                return true;
+            }
+
+            filter.NotSet(t => t.FromId);
+            return false;
+        }
+
+        static bool SetTo(Guid? to, GenericFilterBuilder<Connection> filter)
+        {
+            if (to != null && to.HasValue && to.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.ToId, to);
+                return true;
+            }
+
+            filter.NotSet(t => t.ToId);
+            return false;
+        }
+
+        var isFromSet = SetFrom(from, filter);
+        var isToSet = SetTo(to, filter);
+        if (isFromSet || isToSet)
+        {
+            filter.NotSet(t => t.Id);
+            filter.NotSet(t => t.FacilitatorId);
+        }
+        else
+        {
+            Unreachable();
+        }
+
+        var result = await ConnectionRepository.GetExtended(filter, cancellationToken: cancellationToken);
+        return result?.ToList() ?? [];
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<Assignment>> AddAssignment(Guid from, Guid to, string roleCode, CancellationToken cancellationToken = default)
+    {
+        var dependencies = await GetAssignmentDependencies(from, to, roleCode, cancellationToken);
+        if (ValidateAssignmentData(dependencies.EntityFrom, dependencies.EntityTo, dependencies.Roles) is var problem && problem is { })
+        {
+            return problem;
+        }
+
+        var roleId = dependencies.Roles.First().Id;
+        var filter = AssignmentRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.ToId, to)
+            .Equal(t => t.RoleId, roleId);
+
+        var existingAssignment = await AssignmentRepository.Get(filter, cancellationToken: cancellationToken);
+        if (existingAssignment != null && existingAssignment.Any())
+        {
+            return existingAssignment.First();
+        }
+
+        var assignment = new Assignment()
+        {
+            FromId = from,
+            ToId = to,
+            RoleId = roleId,
+        };
+
+        var result = await AssignmentRepository.Create(assignment, DbAudit.Value, cancellationToken);
+        if (result == 0)
+        {
+            Unreachable();
+        }
+
+        return assignment;
+    }
+
+    /// <inheritdoc />
+    public async Task<ValidationProblemInstance> RemoveAssignment(Guid from, Guid to, string roleCode, bool cascade, CancellationToken cancellationToken = default)
+    {
+        var dependencies = await GetAssignmentDependencies(from, to, roleCode, cancellationToken);
+        if (ValidateAssignmentData(dependencies.EntityFrom, dependencies.EntityTo, dependencies.Roles) is var problem && problem is { })
+        {
+            return problem;
+        }
+
+        var roleId = dependencies.Roles.First().Id;
+        var filter = AssignmentRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.ToId, to)
+            .Equal(t => t.RoleId, roleId);
+
+        var existingAssignments = await AssignmentRepository.Get(filter, cancellationToken: cancellationToken);
+        if (existingAssignments != null && !existingAssignments.Any())
+        {
+            return null;
+        }
+
+        var existingAssignment = existingAssignments.First();
+
+        if (!cascade)
+        {
+            var packages = await AssignmentPackageRepository.Get(f => f.AssignmentId, existingAssignment.Id, cancellationToken: cancellationToken);
+            var delegationsFrom = await DelegationRepository.Get(f => f.FromId, existingAssignment.Id, cancellationToken: cancellationToken);
+            var delegationsTo = await DelegationRepository.Get(f => f.ToId, existingAssignment.Id, cancellationToken: cancellationToken);
+
+            problem = ValidationRules.Validate(
+                ValidationRules.QueryParameters.HasPackagesAssigned(packages),
+                ValidationRules.QueryParameters.HasDelegationsAssigned(delegationsFrom),
+                ValidationRules.QueryParameters.HasDelegationsAssigned(delegationsTo)
+            );
+            if (problem is { })
+            {
+                return problem;
+            }
+        }
+
+        var result = await AssignmentRepository.Delete(existingAssignment.Id, DbAudit.Value, cancellationToken: cancellationToken);
+        if (result == 0)
+        {
+            Unreachable();
+        }
+
+        return null;
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<List<ConnectionPackage>>> GetPackages(Guid? from, Guid? to, CancellationToken cancellationToken = default)
+    {
+        var filter = ConnectionPackageRepository.CreateFilterBuilder();
+        static bool SetFrom(Guid? from, GenericFilterBuilder<ConnectionPackage> filter)
+        {
+            if (from != null && from.HasValue && from.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.FromId, from);
+                return true;
+            }
+
+            filter.NotSet(t => t.FromId);
+            return false;
+        }
+
+        static bool SetTo(Guid? to, GenericFilterBuilder<ConnectionPackage> filter)
+        {
+            if (to != null && to.HasValue && to.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.ToId, to);
+                return true;
+            }
+
+            filter.NotSet(t => t.ToId);
+            return false;
+        }
+
+        var isFromSet = SetFrom(from, filter);
+        var isToSet = SetTo(to, filter);
+        if (isFromSet || isToSet)
+        {
+            filter.NotSet(t => t.Id);
+            filter.NotSet(t => t.FacilitatorId);
+        }
+        else
+        {
+            Unreachable();
+        }
+
+        var result = await ConnectionPackageRepository.Get(filter, cancellationToken: cancellationToken);
+        return result.ToList();
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<AssignmentPackage>> AddPackage(Guid from, Guid to, string roleCode, string packageUrn, CancellationToken cancellationToken = default)
+    {
+        var filter = PackageRepository.CreateFilterBuilder()
+            .Add(t => t.Urn, packageUrn, FilterComparer.EndsWith);
+
+        var packages = await PackageRepository.Get(filter, cancellationToken: cancellationToken);
+        var problem = ValidationRules.Validate(ValidationRules.QueryParameters.PackageUrnLookup(packages));
+        if (problem is { })
+        {
+            return problem;
+        }
+
+        var package = packages.First();
+        return await AddPackage(from, to, roleCode, package.Id, "packgeUrn", cancellationToken);
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Services/ConnectionService.cs
user: login: jonkjetiloye
body: typo: `packgeUrn`
created_at: 2025-05-09T10:22:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,530 @@
+using Altinn.AccessMgmt.Core.Models;
+using Altinn.Authorization.ProblemDetails;
+
+namespace Altinn.AccessManagement.Core.Errors;
+
+/// <summary>
+/// A utility class that provides methods for validating data using a series of rules.
+/// </summary>
+public static class ValidationRules
+{
+    /// <summary>
+    /// A delegate representing a validation rule that accepts a reference to a <see cref="ValidationErrorBuilder"/>
+    /// and adds errors if validation fails.
+    /// </summary>
+    /// <param name="errors">The reference to the <see cref="ValidationErrorBuilder"/> where validation errors are added.</param>
+    public delegate void Func(ref ValidationErrorBuilder errors);
+
+    /// <summary>
+    /// A delegate that returns a validation rule that accepts a reference to a <see cref="ValidationErrorBuilder"/>
+    /// and adds errors if validation fails.
+    /// </summary>
+    public delegate Func FuncExpression();
+
+    /// <summary>
+    /// Validates a series of rules against the provided parameters.
+    /// It executes each rule in sequence and returns a <see cref="ValidationProblemInstance"/>
+    /// if any validation errors are found, or <c>null</c> if no errors exist.
+    /// </summary>
+    /// <param name="rules">An array of validation rules to apply.</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> containing validation errors, if any,
+    /// or <c>null</c> if the validation passed without any errors.
+    /// </returns>
+    public static ValidationProblemInstance? Validate(params FuncExpression[] rules)
+    {
+        var builder = default(ValidationErrorBuilder);
+        All(rules)()(ref builder);
+        builder.TryBuild(out var result);
+        return result;
+    }
+
+    /// <summary>
+    /// Validates a series of rules against the provided parameters.
+    /// It executes each rule in sequence and returns a <see cref="ValidationProblemInstance"/>
+    /// if any validation errors are found, or <c>null</c> if no errors exist.
+    /// </summary>
+    /// <param name="rules">An array of validation rules to apply.</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> containing validation errors, if any,
+    /// or <c>null</c> if the validation passed without any errors.
+    /// </returns>
+    public static ValidationProblemInstance? Validate(params Func[] rules)
+    {
+        var builder = default(ValidationErrorBuilder);
+        foreach (var rule in rules)
+        {
+            rule(ref builder);
+        }
+
+        builder.TryBuild(out var result);
+
+        return result;
+    }
+
+    /// <summary>
+    /// Combines multiple validation rules that must all pass.
+    /// </summary>
+    /// <param name="funcs">The validation functions to combine.</param>
+    /// <returns>A combined validation rule that applies all the specified rules.</returns>
+    public static FuncExpression All(params FuncExpression[] funcs) => () =>
+    {
+        var results = new List<Func>();
+        foreach (var func in funcs)
+        {
+            if (func() is var fn && fn is { })
+            {
+                results.Add(fn);
+            }
+        }
+
+        return (ref ValidationErrorBuilder errors) =>
+        {
+            foreach (var result in results)
+            {
+                result(ref errors);
+            }
+        };
+    };
+
+    /// <summary>
+    /// Combines multiple validation rules where at least one must pass.
+    /// </summary>
+    /// <param name="funcs">The validation functions to combine.</param>
+    /// <returns>A combined validation rule that applies any of the specified rules.</returns>
+    public static FuncExpression Any(params FuncExpression[] funcs) => () =>
+    {
+        var results = new List<Func>();
+        foreach (var func in funcs)
+        {
+            if (func() is var fn && fn is { })
+            {
+                results.Add(fn);
+            }
+        }
+
+        if (results.Count == funcs.Length)
+        {
+            return (ref ValidationErrorBuilder errors) =>
+            {
+                foreach (var result in results)
+                {
+                    result(ref errors);
+                }
+            };
+        }
+
+        return null;
+    };
+
+    /// <summary>
+    /// Validates the default query parameters for an end user.
+    /// </summary>
+    /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+    /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+    /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> with validation errors if any.
+    /// </returns>
+    public static ValidationProblemInstance EnduserAddConnection(string party, string from, string to) => Validate(
+        QueryParameters.Party(party),
+        QueryParameters.PartyFrom(from),
+        QueryParameters.PartyTo(to),
+        QueryParameters.EnduserAddCombination(party, from, to)
+    );
+
+    /// <summary>
+    /// Validates the default query parameters for an end user.
+    /// </summary>
+    /// <param name="userUuid">The UUID of the user performing the action (current user).</param>
+    /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+    /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+    /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> with validation errors if any.
+    /// </returns>
+    public static ValidationProblemInstance EnduserReadConnection(string party, string from, string to) => Validate(
+        QueryParameters.Party(party),
+        QueryParameters.EnduserReadInputCombination(party, from, to),
+        Any(
+            QueryParameters.PartyFrom(from),
+            QueryParameters.PartyTo(to)
+        )
+    );
+
+    /// <summary>
+    /// Validates the default query parameters for an end user.
+    /// </summary>
+    /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+    /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+    /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+    /// <returns>
+    /// A <see cref="ValidationProblemInstance"/> with validation errors if any.
+    /// </returns>
+    public static ValidationProblemInstance EnduserRemoveConnection(string party, string from, string to) => Validate(
+        QueryParameters.Party(party),
+        QueryParameters.PartyFrom(from),
+        QueryParameters.PartyTo(to),
+        QueryParameters.EnduserRemoveCombination(party, from, to)
+    );
+
+    /// <summary>
+    /// Provides validation rules for query parameters.
+    /// </summary>
+    public static class QueryParameters
+    {
+        private static IEnumerable<string> ParamKeywords { get; } = ["me", "all"];
+
+        private static string ParamKeywordsToString { get; } = "<me, all | blank, uuid>";
+
+        /// <summary>
+        /// Checks if any packages exist with given ID.
+        /// </summary>
+        /// <param name="packages">Lists of packages</param>
+        /// <param name="paramName">name of query parameter</param>
+        /// <returns></returns>
+        public static FuncExpression AnyPackages(IEnumerable<ConnectionPackage> packages, string paramName = "packageId") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.Any())
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.InvalidResource, $"QUERY/{paramName}", [new("Packages", $"No packages found.")]);
+        };
+
+        /// <summary>
+        /// Checks the list of packages all <see cref="Package.IsAssignable"/> is set to true.
+        /// </summary>
+        /// <param name="packages">list of packages</param>
+        /// <param name="paramName">name of the query parameter</param>
+        /// <returns></returns>
+        public static FuncExpression PackageIsAssignableByDefinition(IEnumerable<ExtConnectionPackage> packages, string paramName = "packageId") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.All(t => t.Package.IsAssignable))
+            {
+                return null;
+            }
+
+            var packagesNotAssignable = packages
+                .Where(p => p.Package.IsAssignable)
+                .Select(p => p.Id);
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/{paramName}", [new("Packages", $"{string.Join(",", packagesNotAssignable)} are not assignable.")]);
+        };
+
+        /// <summary>
+        /// Checks is packages can be asigned by checking <see cref="ConnectionPackage.CanAssign"/> is set to to true.
+        /// </summary>
+        /// <param name="packages">List of packages.</param>
+        /// <param name="paramName">name of the query parameter.</param>
+        /// <returns></returns>
+        public static FuncExpression PackageIsAssignableByUser(IEnumerable<ConnectionPackage> packages, string paramName = "packageId") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.All(t => t.CanAssign))
+            {
+                return null;
+            }
+
+            var packagesNotAssignableByUser = packages
+                .Where(p => p.CanAssign)
+                .Select(p => p.Id);
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/{paramName}", [new("Packages", $"Packages with IDs {string.Join(",", packagesNotAssignableByUser)} can't be assigned by user.")]);
+        };
+
+        /// <summary>
+        /// Used to check if package exists by check URN and resulkt of the DB lookup.
+        /// </summary>
+        /// <param name="packages">List of packags</param>
+        /// <param name="paramName">name of the query URN parameter.</param>
+        /// <returns></returns>
+        public static FuncExpression PackageUrnLookup(IEnumerable<Package> packages, string paramName = "packageUrn") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages?.Count() == 1)
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.AssignmentIsActiveInOneOrMoreDelegations, $"QUERY/{paramName}", [new("packages", string.Join(",", packages.Select(p => p.Id.ToString())))]);
+        };
+
+        /// <summary>
+        /// Checks if any packages are assigned. Used along with cascade delete
+        /// </summary>
+        /// <param name="packages">List of packages.</param>
+        /// <param name="paramName">name of the query parameter.</param>
+        public static FuncExpression HasPackagesAssigned(IEnumerable<AssignmentPackage> packages, string paramName = "cascade") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(packages);
+            ArgumentException.ThrowIfNullOrEmpty(paramName);
+
+            if (packages.Any())
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.AssignmentIsActiveInOneOrMoreDelegations, $"QUERY/{paramName}", [new("packages", $"following packages has active assignments [{string.Join(",", packages.Select(p => p.Id.ToString()))}].")]);
+            }
+
+            return null;
+        };
+
+        /// <summary>
+        /// Checks whether any packages are assigned. Used in conjunction with cascade delete.
+        /// </summary>
+        /// <param name="packages">List of role assignments.</param>
+        /// <param name="roleCode">The name of the role to verify.</param>
+        /// <param name="paramNameFrom">The name of the source query parameter.</param>
+        /// <param name="paramNameTo">The name of the target query parameter.</param>
+        public static FuncExpression VerifyAssignmentRoleExists(IEnumerable<Assignment> packages, string roleCode, string paramNameFrom = "from", string paramNameTo = "to") => () =>
+        {
+            if (packages is { } && packages.Any())
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+            {
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/{paramNameFrom}",
+                    [new("packages", $"No active role assignments of type '{roleCode}' were found from the value in query parameter '{paramNameFrom}' to the value in query parameter '{paramNameTo}'.")]);
+            };
+        };
+
+        /// <summary>
+        /// Checks if any delegations are given assigned. Used along with cascade delete
+        /// </summary>
+        /// <param name="delegations">List of delegations.</param>
+        /// <param name="paramName">name of the query parameter.</param>
+        public static FuncExpression HasDelegationsAssigned(IEnumerable<Delegation> delegations, string paramName = "cascade") => () =>
+        {
+            ArgumentNullException.ThrowIfNull(delegations);
+            if (delegations.Any())
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.EntityNotExists, $"QUERY/{paramName}", [new("packages", string.Join(",", delegations.Select(p => p.Id.ToString())))]);
+            }
+
+            return null;
+        };
+
+        /// <summary>
+        /// Checks if party exists
+        /// </summary>
+        /// <param name="party"></param>
+        /// <param name="paramName"></param>
+        /// <returns></returns>
+        public static FuncExpression PartyExists(Entity party, string paramName = "party") => () =>
+        {
+            if (party is { })
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.EntityNotExists, $"QUERY/{paramName}", [new("party", $"Entity do not exists.")]);
+        };
+
+        public static FuncExpression PartyIsEntityType(ExtEntity party, string entityType, string paramName = "party") => () =>
+        {
+            if (party is { })
+            {
+                if (party.Type.Name.Equals(entityType, StringComparison.InvariantCultureIgnoreCase))
+                {
+                    return null;
+                }
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+                errors.Add(ValidationErrors.EntityNotExists, $"QUERY/{paramName}", [new("party", $"Entity do not exists or entity is not of type '{entityType}'.")]);
+        };
+
+        /// <summary>
+        /// Validates combination of input parameters
+        /// </summary>
+        public static FuncExpression EnduserAddCombination(string party, string from, string to) => () =>
+        {
+            if (!Guid.TryParse(party, out var partyUuid) || partyUuid == Guid.Empty)
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/party", [new("party", $"Paramater is not a valid a UUID.")]);
+            }
+
+            if (!Guid.TryParse(to, out var fromUuid) || fromUuid == Guid.Empty)
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/party", [new("from", $"Parameter is not a valid UUID.")]);
+            }
+
+            if (!Guid.TryParse(to, out var toUuid) || toUuid == Guid.Empty)
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/party", [new("to", $"Parameter is not a valid UUID.")]);
+            }
+
+            if (partyUuid != fromUuid)
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                {
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/from", [new("from", "must match the 'party' UUID.")]);
+                };
+            }
+
+            return null;
+        };
+
+        /// <summary>
+        /// Validates that the acting party matches either the 'from' or 'to' party,
+        /// and ensures that none of the provided UUIDs are invalid or empty. If any UUID is 
+        /// set to "me", it will be replaced by the user's UUID.
+        /// </summary>
+        /// <param name="useruuid">The UUID of the user performing the action (current user).</param>
+        /// <param name="party">The UUID of the acting party (the user acting on behalf). Can also be "me".</param>
+        /// <param name="from">The UUID of the 'from' party (could be a person or an organization).</param>
+        /// <param name="to">The UUID of the 'to' party (could be a person or an organization).</param>
+        /// <returns>
+        /// A validation rule that adds an error if any UUID is invalid or empty, 
+        /// or if the party does not match either the 'from' or 'to' UUID.
+        /// </returns>
+        public static FuncExpression EnduserReadInputCombination(string party, string from, string to) => () =>
+        {
+            if (!Guid.TryParse(party, out var partyUuid))
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/party", [new("party", $"The 'party' parameter is not valid.")]);
+            }
+
+            if (!string.IsNullOrEmpty(from) && Guid.TryParse(from, out var fromUuid) && fromUuid == partyUuid)
+            {
+                return null;
+            }
+
+            if (!string.IsNullOrEmpty(to) && Guid.TryParse(to, out var toUuid) && toUuid == partyUuid)
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+            {
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/from", [new("from", "Either the 'from' UUID or the 'to' UUID must match the 'party' UUID. Neither matches the 'party' UUID.")]);
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/to", [new("to", "Either the 'to' UUID or the 'from' UUID must match the 'party' UUID. Neither matches the 'party' UUID.")]);
+            };
+        };
+
+        /// <summary>
+        /// Delete combination
+        /// </summary>
+        /// <param name="useruuid"></param>
+        /// <param name="party"></param>
+        /// <param name="from"></param>
+        /// <param name="to"></param>
+        /// <returns></returns>
+        public static FuncExpression EnduserRemoveCombination(string party, string from, string to) => () =>
+        {
+            if (!Guid.TryParse(party, out var partyUuid))
+            {
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/party", [new("party", $"The 'party' parameter is not valid.")]);
+            }
+
+            if (!string.IsNullOrEmpty(from) && Guid.TryParse(from, out var fromUuid) && fromUuid == partyUuid)
+            {
+                return null;
+            }
+
+            return (ref ValidationErrorBuilder errors) =>
+            {
+                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/from", [new("from", "must match the 'party' UUID.")]);
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/EnduserValidationRules.cs
user: login: andreasisnes
body: fixed
created_at: 2025-05-09T10:31:50Z
author_association: COLLABORATOR
diff_hunk: @@ -22,281 +22,184 @@ namespace Altinn.AccessManagement.Api.Enduser.Controllers;
 [Route("accessmanagement/api/v1/enduser/connections")]
 [FeatureGate(AccessManagementEnduserFeatureFlags.ControllerConnections)]
 [Authorize(Policy = AuthzConstants.SCOPE_PORTAL_ENDUSER)]
-public class ConnectionController(IHttpContextAccessor accessor, IConnectionService connectionService, IAssignmentService assignmentService, IEntityRepository entityRepository) : ControllerBase
+public class ConnectionController(IEnduserConnectionService connectionService) : ControllerBase
 {
-    private IHttpContextAccessor Accessor { get; } = accessor;
+    private IEnduserConnectionService ConnectionService { get; } = connectionService;
 
     /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
+    /// Get connections between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpGet]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<AssignmentExternal>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetConnections([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
-        {
-            return BadRequest();
-        }
-
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
-        {
-            // Party must match From or To
-            return BadRequest();
-        }
-
-        var partyUuid = Accessor.GetPartyUuid();
-        var audit = new ChangeRequestOptions()
-        {
-            ChangedBy = partyUuid,
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from.HasValue && to.HasValue)
+        if (ValidationRules.EnduserReadConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Ok(await connectionService.Get(fromId: from.Value, toId: to.Value, facilitatorId: null, cancellationToken: cancellationToken));
+            return problem.ToActionResult();
         }
 
-        if (from.HasValue)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        var result = await ConnectionService.Get(fromUuid, toUuid, cancellationToken: cancellationToken);
+        if (result.IsProblem)
         {
-            return Ok(await connectionService.GetReceived(from.Value, cancellationToken: cancellationToken));
+            return result.Problem.ToActionResult();
         }
 
-        if (to.HasValue)
-        {
-            return Ok(await connectionService.GetGiven(to.Value, cancellationToken: cancellationToken));
-        }
-        
-        return BadRequest();
+        return Ok(PaginatedResult.Create(result.Value, null));
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
     [HttpPost]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, CancellationToken cancellationToken = default)
+    [ProducesResponseType<Assignment>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddAssignment([FromQuery] ConnectionInput connection, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from != party)
-        {
-            throw new Exception("From party does not match from");
-        }
-
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
-        {
-            return Problem("From party not found");
-        }
-
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("From must be of type 'Organisasjon'");
-        }
-
-        //// To must be Type:Organisasjon
-        //// #550:AC:Det skal bare være mulig å legge til Organisasjoner som ny Rightholder
-        var toEntity = await entityRepository.GetExtended(to);
-        if (toEntity == null)
+        if (ValidationRules.EnduserAddConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Problem("To party not found");
+            return problem.ToActionResult();
         }
 
-        if (!toEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        var result = await ConnectionService.AddAssignment(fromUuid, toUuid, "rettighetshaver", cancellationToken);
+        if (result.IsProblem)
         {
-            return Problem("To must be of type 'Organisasjon'");
+            return result.Problem.ToActionResult();
         }
 
-        var res = await assignmentService.GetOrCreateAssignmentInternal(fromId: from, toId: to, roleCode: "rettighetshaver", options, cancellationToken: cancellationToken);
-
-        if (res != null)
-        {
-            return Ok(res);
-        }
-
-        return Problem("Unable add connection");
+        return Ok(result.Value);
     }
 
     /// <summary>
     /// Remove package from connection (assignment or delegation)
     /// </summary>
     [HttpDelete]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<ProblemInstance> RemoveConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> RemoveAssignment([FromQuery] ConnectionInput connection, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (!(from == party) && !(to == party))
+        if (ValidationRules.EnduserRemoveConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            throw new Exception("From party does not match from or to");
+            return problem.ToActionResult();
         }
 
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        problem = await ConnectionService.RemoveAssignment(fromUuid, toUuid, "rettighetshaver", cascade, cancellationToken);
+        if (problem is { })
         {
-            throw new Exception("From party not found");
+            problem.ToActionResult();
         }
 
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            throw new Exception("From must be of type 'Organisasjon'");
-        }
-
-        return await assignmentService.DeleteAssignment(fromId: from, toId: to, roleCode: "rettighetshaver", options, cascade: cascade, cancellationToken);
+        return NoContent();
     }
 
     /// <summary>
     /// Creates an assignment between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
-    [HttpGet]
-    [Route("accesspackages")]
+    [HttpGet("accesspackages")]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [ProducesResponseType<PaginatedResult<ConnectionPackage>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetPackages([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetPackages([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
+        if (ValidationRules.EnduserReadConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return BadRequest();
+            return problem.ToActionResult();
         }
 
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        var result = await ConnectionService.GetPackages(fromUuid, toUuid, cancellationToken);
+        if (result.IsProblem)
         {
-            // Party must match From or To
-            return BadRequest();
+            return result.Problem.ToActionResult();
         }
 
-        var res = await connectionService.GetPackages(fromId: from, toId: to);
-
-        return Ok(res);
+        return Ok(PaginatedResult.Create(result.Value, null));
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
-    [HttpPost]
-    [Route("accesspackages")]
+    [HttpPost("accesspackages")]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddPackages([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] Guid? package, [FromQuery] string packageUrn, CancellationToken cancellationToken = default)
+    [ProducesResponseType<AssignmentPackage>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddPackages([FromQuery] ConnectionInput connection, [FromQuery] Guid? packageId, [FromQuery] string packageUrn, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from != party)
-        {
-            // Party must match From or To
-            return BadRequest();
-        }
-
-        //// From must by Type:Organisasjon
-        //// #568:AC:From party må være en Organisasjon (skal ikke være mulig å delegere fra privatperson el. andre entitetstyper enda)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
-        {
-            throw new Exception("From party not found");
-        }
-
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
+        if (ValidationRules.EnduserAddConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            throw new Exception("From must be of type 'Organisasjon'");
+            return problem.ToActionResult();
         }
 
-        //// To must be Type:Organisasjon
-        //// #568:AC:To party må være en Organisasjon (skal ikke være mulig å delegere fra privatperson el. andre entitetstyper enda)
-        var toEntity = await entityRepository.GetExtended(to);
-        if (toEntity == null)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        async Task<Result<AssignmentPackage>> AddPackage()
         {
-            return Problem("To party not found");
-        }
-
-        if (!toEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("To must be of type 'Organisasjon'");
-        }
-
-        if (package.HasValue)
-        {
-            var res = await connectionService.AddPackage(fromId: from, toId: to, roleCode: "rettighetshaver", packageId: package.Value, options);
-            if (res)
+            if (packageId.HasValue)
             {
-                return Ok();
+                return await ConnectionService.AddPackage(fromUuid, toUuid, "rettighetshaver", packageId.Value, cancellationToken);
             }
+
+            return await ConnectionService.AddPackage(fromUuid, toUuid, "rettighetshaver", packageUrn, cancellationToken);
         }
-        else
+
+        var result = await AddPackage();
+        if (result.IsProblem)
         {
-            packageUrn = packageUrn.ToLower().StartsWith("urn:") ? packageUrn : ":" + packageUrn;
-            var res = await connectionService.AddPackage(fromId: from, toId: to, roleCode: "rettighetshaver", packageUrn: packageUrn, options);
-            if (res)
-            {
-                return Ok();
-            }
+            result.Problem.ToActionResult();
         }
 
-        return Problem("Unable to remove package");
+        return Ok(result.Value);
     }
 
     /// <summary>
     /// Remove package from connection (assignment or delegation)
     /// </summary>
-    [HttpDelete]
-    [Route("accesspackages")]
+    [HttpDelete("accesspackages")]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> RemovePackages([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] Guid package, CancellationToken cancellationToken = default)
+    [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> RemovePackages([FromQuery] ConnectionInput connection, [FromQuery] Guid package, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (!(from == party) && !(to == party))
+        if (ValidationRules.EnduserAddConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
user: login: andreasisnes
body: stemmer
created_at: 2025-05-09T10:31:58Z
author_association: COLLABORATOR
diff_hunk: @@ -441,15 +438,28 @@ public static FuncExpression EnduserRemoveCombination(string party, string from,
                     errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/party", [new("party", $"The 'party' parameter is not valid.")]);
             }
 
-            if (!string.IsNullOrEmpty(from) && Guid.TryParse(from, out var fromUuid) && fromUuid == partyUuid)
+            if (!Guid.TryParse(from, out var fromUuid) || fromUuid == Guid.Empty)
             {
-                return null;
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/from", [new("from", $"Parameter is not a valid UUID.")]);
             }
 
-            return (ref ValidationErrorBuilder errors) =>
+            if (!Guid.TryParse(to, out var toUuid) || toUuid == Guid.Empty)
             {
-                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/from", [new("from", "must match the 'party' UUID.")]);
-            };
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/to", [new("to", $"Parameter is not a valid UUID.")]);
+            }
+
+            if (partyUuid != fromUuid || partyUuid != toUuid)
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/EnduserValidationRules.cs
user: login: jonkjetiloye
body: må ikke dette være && ikke || ?
created_at: 2025-05-09T10:39:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,417 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using Altinn.AccessManagement.Core.Errors;
+using Altinn.AccessMgmt.Core.Models;
+using Altinn.AccessMgmt.Persistence.Core.Helpers;
+using Altinn.AccessMgmt.Persistence.Repositories.Contracts;
+using Altinn.AccessMgmt.Persistence.Services;
+using Altinn.Authorization.ProblemDetails;
+
+namespace Altinn.AccessManagement.Enduser.Services;
+
+/// <summary>
+/// Service for managing connections.
+/// </summary>
+public class ConnectionService(
+    IDbAudit dbAudit,
+    IConnectionRepository connectionRepository,
+    IConnectionPackageRepository connectionPackageRepository,
+    IPackageRepository packageRepository,
+    IRoleRepository roleRepository,
+    IAssignmentRepository assignmentRepository,
+    IDelegationRepository delegationRepository,
+    IAssignmentPackageRepository assignmentPackageRepository,
+    IEntityRepository entityRepository
+    ) : IEnduserConnectionService
+{
+    private IDbAudit DbAudit { get; } = dbAudit;
+
+    private IConnectionRepository ConnectionRepository { get; } = connectionRepository;
+
+    private IConnectionPackageRepository ConnectionPackageRepository { get; } = connectionPackageRepository;
+
+    private IPackageRepository PackageRepository { get; } = packageRepository;
+
+    private IRoleRepository RoleRepository { get; } = roleRepository;
+
+    private IAssignmentRepository AssignmentRepository { get; } = assignmentRepository;
+
+    private IDelegationRepository DelegationRepository { get; } = delegationRepository;
+
+    private IAssignmentPackageRepository AssignmentPackageRepository { get; } = assignmentPackageRepository;
+
+    private IEntityRepository EntityRepository { get; } = entityRepository;
+
+    /// <inheritdoc />
+    public async Task<Result<List<ExtConnection>>> Get(Guid? from = null, Guid? to = null, CancellationToken cancellationToken = default)
+    {
+        var filter = ConnectionRepository.CreateFilterBuilder();
+        static bool SetFrom(Guid? from, GenericFilterBuilder<Connection> filter)
+        {
+            if (from != null && from.HasValue && from.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.FromId, from);
+                return true;
+            }
+
+            filter.NotSet(t => t.FromId);
+            return false;
+        }
+
+        static bool SetTo(Guid? to, GenericFilterBuilder<Connection> filter)
+        {
+            if (to != null && to.HasValue && to.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.ToId, to);
+                return true;
+            }
+
+            filter.NotSet(t => t.ToId);
+            return false;
+        }
+
+        var isFromSet = SetFrom(from, filter);
+        var isToSet = SetTo(to, filter);
+        if (isFromSet || isToSet)
+        {
+            filter.NotSet(t => t.Id);
+            filter.NotSet(t => t.FacilitatorId);
+        }
+        else
+        {
+            Unreachable();
+        }
+
+        var result = await ConnectionRepository.GetExtended(filter, cancellationToken: cancellationToken);
+        return result?.ToList() ?? [];
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<Assignment>> AddAssignment(Guid from, Guid to, string roleCode, CancellationToken cancellationToken = default)
+    {
+        var dependencies = await GetAssignmentDependencies(from, to, roleCode, cancellationToken);
+        if (ValidateAssignmentData(dependencies.EntityFrom, dependencies.EntityTo, dependencies.Roles) is var problem && problem is { })
+        {
+            return problem;
+        }
+
+        var roleId = dependencies.Roles.First().Id;
+        var filter = AssignmentRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.ToId, to)
+            .Equal(t => t.RoleId, roleId);
+
+        var existingAssignment = await AssignmentRepository.Get(filter, cancellationToken: cancellationToken);
+        if (existingAssignment != null && existingAssignment.Any())
+        {
+            return existingAssignment.First();
+        }
+
+        var assignment = new Assignment()
+        {
+            FromId = from,
+            ToId = to,
+            RoleId = roleId,
+        };
+
+        var result = await AssignmentRepository.Create(assignment, DbAudit.Value, cancellationToken);
+        if (result == 0)
+        {
+            Unreachable();
+        }
+
+        return assignment;
+    }
+
+    /// <inheritdoc />
+    public async Task<ValidationProblemInstance> RemoveAssignment(Guid from, Guid to, string roleCode, bool cascade, CancellationToken cancellationToken = default)
+    {
+        var dependencies = await GetAssignmentDependencies(from, to, roleCode, cancellationToken);
+        if (ValidateAssignmentData(dependencies.EntityFrom, dependencies.EntityTo, dependencies.Roles) is var problem && problem is { })
+        {
+            return problem;
+        }
+
+        var roleId = dependencies.Roles.First().Id;
+        var filter = AssignmentRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.ToId, to)
+            .Equal(t => t.RoleId, roleId);
+
+        var existingAssignments = await AssignmentRepository.Get(filter, cancellationToken: cancellationToken);
+        if (existingAssignments != null && !existingAssignments.Any())
+        {
+            return null;
+        }
+
+        var existingAssignment = existingAssignments.First();
+
+        if (!cascade)
+        {
+            var packages = await AssignmentPackageRepository.Get(f => f.AssignmentId, existingAssignment.Id, cancellationToken: cancellationToken);
+            var delegationsFrom = await DelegationRepository.Get(f => f.FromId, existingAssignment.Id, cancellationToken: cancellationToken);
+            var delegationsTo = await DelegationRepository.Get(f => f.ToId, existingAssignment.Id, cancellationToken: cancellationToken);
+
+            problem = ValidationRules.Validate(
+                ValidationRules.QueryParameters.HasPackagesAssigned(packages),
+                ValidationRules.QueryParameters.HasDelegationsAssigned(delegationsFrom),
+                ValidationRules.QueryParameters.HasDelegationsAssigned(delegationsTo)
+            );
+            if (problem is { })
+            {
+                return problem;
+            }
+        }
+
+        var result = await AssignmentRepository.Delete(existingAssignment.Id, DbAudit.Value, cancellationToken: cancellationToken);
+        if (result == 0)
+        {
+            Unreachable();
+        }
+
+        return null;
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<List<ConnectionPackage>>> GetPackages(Guid? from, Guid? to, CancellationToken cancellationToken = default)
+    {
+        var filter = ConnectionPackageRepository.CreateFilterBuilder();
+        static bool SetFrom(Guid? from, GenericFilterBuilder<ConnectionPackage> filter)
+        {
+            if (from != null && from.HasValue && from.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.FromId, from);
+                return true;
+            }
+
+            filter.NotSet(t => t.FromId);
+            return false;
+        }
+
+        static bool SetTo(Guid? to, GenericFilterBuilder<ConnectionPackage> filter)
+        {
+            if (to != null && to.HasValue && to.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.ToId, to);
+                return true;
+            }
+
+            filter.NotSet(t => t.ToId);
+            return false;
+        }
+
+        var isFromSet = SetFrom(from, filter);
+        var isToSet = SetTo(to, filter);
+        if (isFromSet || isToSet)
+        {
+            filter.NotSet(t => t.Id);
+            filter.NotSet(t => t.FacilitatorId);
+        }
+        else
+        {
+            Unreachable();
+        }
+
+        var result = await ConnectionPackageRepository.Get(filter, cancellationToken: cancellationToken);
+        return result.ToList();
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<AssignmentPackage>> AddPackage(Guid from, Guid to, string roleCode, string packageUrn, CancellationToken cancellationToken = default)
+    {
+        var filter = PackageRepository.CreateFilterBuilder()
+            .Add(t => t.Urn, packageUrn, FilterComparer.EndsWith);
+
+        var packages = await PackageRepository.Get(filter, cancellationToken: cancellationToken);
+        var problem = ValidationRules.Validate(ValidationRules.QueryParameters.PackageUrnLookup(packages));
+        if (problem is { })
+        {
+            return problem;
+        }
+
+        var package = packages.First();
+        return await AddPackage(from, to, roleCode, package.Id, "packageUrn", cancellationToken);
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<AssignmentPackage>> AddPackage(Guid from, Guid to, string roleCode, Guid packageId, CancellationToken cancellationToken = default)
+    {
+        return await AddPackage(from, to, roleCode, packageId, "packageId", cancellationToken);
+    }
+
+    private async Task<Result<AssignmentPackage>> AddPackage(Guid from, Guid to, string roleCode, Guid packageId, string queryParamName, CancellationToken cancellationToken)
+    {
+        var dependencies = await GetAssignmentDependencies(from, to, roleCode, cancellationToken);
+        if (ValidateAssignmentData(dependencies.EntityFrom, dependencies.EntityTo, dependencies.Roles) is var problem && problem is { })
+        {
+            return problem;
+        }
+
+        var roleId = dependencies.Roles.First().Id;
+        var filter = AssignmentRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.ToId, to)
+            .Equal(t => t.RoleId, roleId);
+
+        var existingAssignments = await AssignmentRepository.Get(filter, cancellationToken: cancellationToken);
+        problem = ValidationRules.Validate(ValidationRules.QueryParameters.VerifyAssignmentRoleExists(existingAssignments, roleCode));
+        if (problem is { })
+        {
+            return problem;
+        }
+
+        var assignment = existingAssignments.First();
+
+        var connectionPackageFilter = ConnectionPackageRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.ToId, to)
+            .Equal(t => t.PackageId, packageId);
+
+        var connectionPackages = await ConnectionPackageRepository.GetExtended(connectionPackageFilter, cancellationToken: cancellationToken);
+
+        problem = ValidationRules.Validate(
+            ValidationRules.QueryParameters.AnyPackages(connectionPackages, queryParamName),
+            ValidationRules.QueryParameters.PackageIsAssignableByUser(connectionPackages, queryParamName),
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Services/ConnectionService.cs
user: login: jonkjetiloye
body: Sliter litt med å forstå logikken som ligger til grunn for å validere om PackageIsAssignableByUser:

Oppslaget av connectionPackages som gjøres her ser ut til å bli gjort basert på From og To som ny AssignmentPackage skal gjøres til (altså om denne eksisterer fra før av?).

Ikke hvor vidt autentisert bruker har rolle-kobling til pakken med CanDelegate=true 🤔 
created_at: 2025-05-09T10:56:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,6 @@
+namespace Altinn.AccessManagement.Api.Enduser.Models;
+
+public class ConnectionExternal
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Models/ConnectionExternal.cs
user: login: andreasisnes
body: slettet modell
created_at: 2025-05-09T11:08:38Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,417 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using Altinn.AccessManagement.Core.Errors;
+using Altinn.AccessMgmt.Core.Models;
+using Altinn.AccessMgmt.Persistence.Core.Helpers;
+using Altinn.AccessMgmt.Persistence.Repositories.Contracts;
+using Altinn.AccessMgmt.Persistence.Services;
+using Altinn.Authorization.ProblemDetails;
+
+namespace Altinn.AccessManagement.Enduser.Services;
+
+/// <summary>
+/// Service for managing connections.
+/// </summary>
+public class ConnectionService(
+    IDbAudit dbAudit,
+    IConnectionRepository connectionRepository,
+    IConnectionPackageRepository connectionPackageRepository,
+    IPackageRepository packageRepository,
+    IRoleRepository roleRepository,
+    IAssignmentRepository assignmentRepository,
+    IDelegationRepository delegationRepository,
+    IAssignmentPackageRepository assignmentPackageRepository,
+    IEntityRepository entityRepository
+    ) : IEnduserConnectionService
+{
+    private IDbAudit DbAudit { get; } = dbAudit;
+
+    private IConnectionRepository ConnectionRepository { get; } = connectionRepository;
+
+    private IConnectionPackageRepository ConnectionPackageRepository { get; } = connectionPackageRepository;
+
+    private IPackageRepository PackageRepository { get; } = packageRepository;
+
+    private IRoleRepository RoleRepository { get; } = roleRepository;
+
+    private IAssignmentRepository AssignmentRepository { get; } = assignmentRepository;
+
+    private IDelegationRepository DelegationRepository { get; } = delegationRepository;
+
+    private IAssignmentPackageRepository AssignmentPackageRepository { get; } = assignmentPackageRepository;
+
+    private IEntityRepository EntityRepository { get; } = entityRepository;
+
+    /// <inheritdoc />
+    public async Task<Result<List<ExtConnection>>> Get(Guid? from = null, Guid? to = null, CancellationToken cancellationToken = default)
+    {
+        var filter = ConnectionRepository.CreateFilterBuilder();
+        static bool SetFrom(Guid? from, GenericFilterBuilder<Connection> filter)
+        {
+            if (from != null && from.HasValue && from.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.FromId, from);
+                return true;
+            }
+
+            filter.NotSet(t => t.FromId);
+            return false;
+        }
+
+        static bool SetTo(Guid? to, GenericFilterBuilder<Connection> filter)
+        {
+            if (to != null && to.HasValue && to.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.ToId, to);
+                return true;
+            }
+
+            filter.NotSet(t => t.ToId);
+            return false;
+        }
+
+        var isFromSet = SetFrom(from, filter);
+        var isToSet = SetTo(to, filter);
+        if (isFromSet || isToSet)
+        {
+            filter.NotSet(t => t.Id);
+            filter.NotSet(t => t.FacilitatorId);
+        }
+        else
+        {
+            Unreachable();
+        }
+
+        var result = await ConnectionRepository.GetExtended(filter, cancellationToken: cancellationToken);
+        return result?.ToList() ?? [];
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<Assignment>> AddAssignment(Guid from, Guid to, string roleCode, CancellationToken cancellationToken = default)
+    {
+        var dependencies = await GetAssignmentDependencies(from, to, roleCode, cancellationToken);
+        if (ValidateAssignmentData(dependencies.EntityFrom, dependencies.EntityTo, dependencies.Roles) is var problem && problem is { })
+        {
+            return problem;
+        }
+
+        var roleId = dependencies.Roles.First().Id;
+        var filter = AssignmentRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.ToId, to)
+            .Equal(t => t.RoleId, roleId);
+
+        var existingAssignment = await AssignmentRepository.Get(filter, cancellationToken: cancellationToken);
+        if (existingAssignment != null && existingAssignment.Any())
+        {
+            return existingAssignment.First();
+        }
+
+        var assignment = new Assignment()
+        {
+            FromId = from,
+            ToId = to,
+            RoleId = roleId,
+        };
+
+        var result = await AssignmentRepository.Create(assignment, DbAudit.Value, cancellationToken);
+        if (result == 0)
+        {
+            Unreachable();
+        }
+
+        return assignment;
+    }
+
+    /// <inheritdoc />
+    public async Task<ValidationProblemInstance> RemoveAssignment(Guid from, Guid to, string roleCode, bool cascade, CancellationToken cancellationToken = default)
+    {
+        var dependencies = await GetAssignmentDependencies(from, to, roleCode, cancellationToken);
+        if (ValidateAssignmentData(dependencies.EntityFrom, dependencies.EntityTo, dependencies.Roles) is var problem && problem is { })
+        {
+            return problem;
+        }
+
+        var roleId = dependencies.Roles.First().Id;
+        var filter = AssignmentRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.ToId, to)
+            .Equal(t => t.RoleId, roleId);
+
+        var existingAssignments = await AssignmentRepository.Get(filter, cancellationToken: cancellationToken);
+        if (existingAssignments != null && !existingAssignments.Any())
+        {
+            return null;
+        }
+
+        var existingAssignment = existingAssignments.First();
+
+        if (!cascade)
+        {
+            var packages = await AssignmentPackageRepository.Get(f => f.AssignmentId, existingAssignment.Id, cancellationToken: cancellationToken);
+            var delegationsFrom = await DelegationRepository.Get(f => f.FromId, existingAssignment.Id, cancellationToken: cancellationToken);
+            var delegationsTo = await DelegationRepository.Get(f => f.ToId, existingAssignment.Id, cancellationToken: cancellationToken);
+
+            problem = ValidationRules.Validate(
+                ValidationRules.QueryParameters.HasPackagesAssigned(packages),
+                ValidationRules.QueryParameters.HasDelegationsAssigned(delegationsFrom),
+                ValidationRules.QueryParameters.HasDelegationsAssigned(delegationsTo)
+            );
+            if (problem is { })
+            {
+                return problem;
+            }
+        }
+
+        var result = await AssignmentRepository.Delete(existingAssignment.Id, DbAudit.Value, cancellationToken: cancellationToken);
+        if (result == 0)
+        {
+            Unreachable();
+        }
+
+        return null;
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<List<ConnectionPackage>>> GetPackages(Guid? from, Guid? to, CancellationToken cancellationToken = default)
+    {
+        var filter = ConnectionPackageRepository.CreateFilterBuilder();
+        static bool SetFrom(Guid? from, GenericFilterBuilder<ConnectionPackage> filter)
+        {
+            if (from != null && from.HasValue && from.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.FromId, from);
+                return true;
+            }
+
+            filter.NotSet(t => t.FromId);
+            return false;
+        }
+
+        static bool SetTo(Guid? to, GenericFilterBuilder<ConnectionPackage> filter)
+        {
+            if (to != null && to.HasValue && to.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.ToId, to);
+                return true;
+            }
+
+            filter.NotSet(t => t.ToId);
+            return false;
+        }
+
+        var isFromSet = SetFrom(from, filter);
+        var isToSet = SetTo(to, filter);
+        if (isFromSet || isToSet)
+        {
+            filter.NotSet(t => t.Id);
+            filter.NotSet(t => t.FacilitatorId);
+        }
+        else
+        {
+            Unreachable();
+        }
+
+        var result = await ConnectionPackageRepository.Get(filter, cancellationToken: cancellationToken);
+        return result.ToList();
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<AssignmentPackage>> AddPackage(Guid from, Guid to, string roleCode, string packageUrn, CancellationToken cancellationToken = default)
+    {
+        var filter = PackageRepository.CreateFilterBuilder()
+            .Add(t => t.Urn, packageUrn, FilterComparer.EndsWith);
+
+        var packages = await PackageRepository.Get(filter, cancellationToken: cancellationToken);
+        var problem = ValidationRules.Validate(ValidationRules.QueryParameters.PackageUrnLookup(packages));
+        if (problem is { })
+        {
+            return problem;
+        }
+
+        var package = packages.First();
+        return await AddPackage(from, to, roleCode, package.Id, "packgeUrn", cancellationToken);
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Services/ConnectionService.cs
user: login: andreasisnes
body: fiksa
created_at: 2025-05-09T11:08:46Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,417 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using Altinn.AccessManagement.Core.Errors;
+using Altinn.AccessMgmt.Core.Models;
+using Altinn.AccessMgmt.Persistence.Core.Helpers;
+using Altinn.AccessMgmt.Persistence.Repositories.Contracts;
+using Altinn.AccessMgmt.Persistence.Services;
+using Altinn.Authorization.ProblemDetails;
+
+namespace Altinn.AccessManagement.Enduser.Services;
+
+/// <summary>
+/// Service for managing connections.
+/// </summary>
+public class ConnectionService(
+    IDbAudit dbAudit,
+    IConnectionRepository connectionRepository,
+    IConnectionPackageRepository connectionPackageRepository,
+    IPackageRepository packageRepository,
+    IRoleRepository roleRepository,
+    IAssignmentRepository assignmentRepository,
+    IDelegationRepository delegationRepository,
+    IAssignmentPackageRepository assignmentPackageRepository,
+    IEntityRepository entityRepository
+    ) : IEnduserConnectionService
+{
+    private IDbAudit DbAudit { get; } = dbAudit;
+
+    private IConnectionRepository ConnectionRepository { get; } = connectionRepository;
+
+    private IConnectionPackageRepository ConnectionPackageRepository { get; } = connectionPackageRepository;
+
+    private IPackageRepository PackageRepository { get; } = packageRepository;
+
+    private IRoleRepository RoleRepository { get; } = roleRepository;
+
+    private IAssignmentRepository AssignmentRepository { get; } = assignmentRepository;
+
+    private IDelegationRepository DelegationRepository { get; } = delegationRepository;
+
+    private IAssignmentPackageRepository AssignmentPackageRepository { get; } = assignmentPackageRepository;
+
+    private IEntityRepository EntityRepository { get; } = entityRepository;
+
+    /// <inheritdoc />
+    public async Task<Result<List<ExtConnection>>> Get(Guid? from = null, Guid? to = null, CancellationToken cancellationToken = default)
+    {
+        var filter = ConnectionRepository.CreateFilterBuilder();
+        static bool SetFrom(Guid? from, GenericFilterBuilder<Connection> filter)
+        {
+            if (from != null && from.HasValue && from.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.FromId, from);
+                return true;
+            }
+
+            filter.NotSet(t => t.FromId);
+            return false;
+        }
+
+        static bool SetTo(Guid? to, GenericFilterBuilder<Connection> filter)
+        {
+            if (to != null && to.HasValue && to.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.ToId, to);
+                return true;
+            }
+
+            filter.NotSet(t => t.ToId);
+            return false;
+        }
+
+        var isFromSet = SetFrom(from, filter);
+        var isToSet = SetTo(to, filter);
+        if (isFromSet || isToSet)
+        {
+            filter.NotSet(t => t.Id);
+            filter.NotSet(t => t.FacilitatorId);
+        }
+        else
+        {
+            Unreachable();
+        }
+
+        var result = await ConnectionRepository.GetExtended(filter, cancellationToken: cancellationToken);
+        return result?.ToList() ?? [];
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<Assignment>> AddAssignment(Guid from, Guid to, string roleCode, CancellationToken cancellationToken = default)
+    {
+        var dependencies = await GetAssignmentDependencies(from, to, roleCode, cancellationToken);
+        if (ValidateAssignmentData(dependencies.EntityFrom, dependencies.EntityTo, dependencies.Roles) is var problem && problem is { })
+        {
+            return problem;
+        }
+
+        var roleId = dependencies.Roles.First().Id;
+        var filter = AssignmentRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.ToId, to)
+            .Equal(t => t.RoleId, roleId);
+
+        var existingAssignment = await AssignmentRepository.Get(filter, cancellationToken: cancellationToken);
+        if (existingAssignment != null && existingAssignment.Any())
+        {
+            return existingAssignment.First();
+        }
+
+        var assignment = new Assignment()
+        {
+            FromId = from,
+            ToId = to,
+            RoleId = roleId,
+        };
+
+        var result = await AssignmentRepository.Create(assignment, DbAudit.Value, cancellationToken);
+        if (result == 0)
+        {
+            Unreachable();
+        }
+
+        return assignment;
+    }
+
+    /// <inheritdoc />
+    public async Task<ValidationProblemInstance> RemoveAssignment(Guid from, Guid to, string roleCode, bool cascade, CancellationToken cancellationToken = default)
+    {
+        var dependencies = await GetAssignmentDependencies(from, to, roleCode, cancellationToken);
+        if (ValidateAssignmentData(dependencies.EntityFrom, dependencies.EntityTo, dependencies.Roles) is var problem && problem is { })
+        {
+            return problem;
+        }
+
+        var roleId = dependencies.Roles.First().Id;
+        var filter = AssignmentRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.ToId, to)
+            .Equal(t => t.RoleId, roleId);
+
+        var existingAssignments = await AssignmentRepository.Get(filter, cancellationToken: cancellationToken);
+        if (existingAssignments != null && !existingAssignments.Any())
+        {
+            return null;
+        }
+
+        var existingAssignment = existingAssignments.First();
+
+        if (!cascade)
+        {
+            var packages = await AssignmentPackageRepository.Get(f => f.AssignmentId, existingAssignment.Id, cancellationToken: cancellationToken);
+            var delegationsFrom = await DelegationRepository.Get(f => f.FromId, existingAssignment.Id, cancellationToken: cancellationToken);
+            var delegationsTo = await DelegationRepository.Get(f => f.ToId, existingAssignment.Id, cancellationToken: cancellationToken);
+
+            problem = ValidationRules.Validate(
+                ValidationRules.QueryParameters.HasPackagesAssigned(packages),
+                ValidationRules.QueryParameters.HasDelegationsAssigned(delegationsFrom),
+                ValidationRules.QueryParameters.HasDelegationsAssigned(delegationsTo)
+            );
+            if (problem is { })
+            {
+                return problem;
+            }
+        }
+
+        var result = await AssignmentRepository.Delete(existingAssignment.Id, DbAudit.Value, cancellationToken: cancellationToken);
+        if (result == 0)
+        {
+            Unreachable();
+        }
+
+        return null;
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<List<ConnectionPackage>>> GetPackages(Guid? from, Guid? to, CancellationToken cancellationToken = default)
+    {
+        var filter = ConnectionPackageRepository.CreateFilterBuilder();
+        static bool SetFrom(Guid? from, GenericFilterBuilder<ConnectionPackage> filter)
+        {
+            if (from != null && from.HasValue && from.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.FromId, from);
+                return true;
+            }
+
+            filter.NotSet(t => t.FromId);
+            return false;
+        }
+
+        static bool SetTo(Guid? to, GenericFilterBuilder<ConnectionPackage> filter)
+        {
+            if (to != null && to.HasValue && to.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.ToId, to);
+                return true;
+            }
+
+            filter.NotSet(t => t.ToId);
+            return false;
+        }
+
+        var isFromSet = SetFrom(from, filter);
+        var isToSet = SetTo(to, filter);
+        if (isFromSet || isToSet)
+        {
+            filter.NotSet(t => t.Id);
+            filter.NotSet(t => t.FacilitatorId);
+        }
+        else
+        {
+            Unreachable();
+        }
+
+        var result = await ConnectionPackageRepository.Get(filter, cancellationToken: cancellationToken);
+        return result.ToList();
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<AssignmentPackage>> AddPackage(Guid from, Guid to, string roleCode, string packageUrn, CancellationToken cancellationToken = default)
+    {
+        var filter = PackageRepository.CreateFilterBuilder()
+            .Add(t => t.Urn, packageUrn, FilterComparer.EndsWith);
+
+        var packages = await PackageRepository.Get(filter, cancellationToken: cancellationToken);
+        var problem = ValidationRules.Validate(ValidationRules.QueryParameters.PackageUrnLookup(packages));
+        if (problem is { })
+        {
+            return problem;
+        }
+
+        var package = packages.First();
+        return await AddPackage(from, to, roleCode, package.Id, "packageUrn", cancellationToken);
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<AssignmentPackage>> AddPackage(Guid from, Guid to, string roleCode, Guid packageId, CancellationToken cancellationToken = default)
+    {
+        return await AddPackage(from, to, roleCode, packageId, "packageId", cancellationToken);
+    }
+
+    private async Task<Result<AssignmentPackage>> AddPackage(Guid from, Guid to, string roleCode, Guid packageId, string queryParamName, CancellationToken cancellationToken)
+    {
+        var dependencies = await GetAssignmentDependencies(from, to, roleCode, cancellationToken);
+        if (ValidateAssignmentData(dependencies.EntityFrom, dependencies.EntityTo, dependencies.Roles) is var problem && problem is { })
+        {
+            return problem;
+        }
+
+        var roleId = dependencies.Roles.First().Id;
+        var filter = AssignmentRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.ToId, to)
+            .Equal(t => t.RoleId, roleId);
+
+        var existingAssignments = await AssignmentRepository.Get(filter, cancellationToken: cancellationToken);
+        problem = ValidationRules.Validate(ValidationRules.QueryParameters.VerifyAssignmentRoleExists(existingAssignments, roleCode));
+        if (problem is { })
+        {
+            return problem;
+        }
+
+        var assignment = existingAssignments.First();
+
+        var connectionPackageFilter = ConnectionPackageRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.ToId, to)
+            .Equal(t => t.PackageId, packageId);
+
+        var connectionPackages = await ConnectionPackageRepository.GetExtended(connectionPackageFilter, cancellationToken: cancellationToken);
+
+        problem = ValidationRules.Validate(
+            ValidationRules.QueryParameters.AnyPackages(connectionPackages, queryParamName),
+            ValidationRules.QueryParameters.PackageIsAssignableByUser(connectionPackages, queryParamName),
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Services/ConnectionService.cs
user: login: andreasisnes
body: validering og logikk jeg tok fra gamle service laget. Tenker du at vi skal fjerne denne sjekken ?
created_at: 2025-05-09T11:27:47Z
author_association: COLLABORATOR
diff_hunk: @@ -441,15 +438,28 @@ public static FuncExpression EnduserRemoveCombination(string party, string from,
                     errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/party", [new("party", $"The 'party' parameter is not valid.")]);
             }
 
-            if (!string.IsNullOrEmpty(from) && Guid.TryParse(from, out var fromUuid) && fromUuid == partyUuid)
+            if (!Guid.TryParse(from, out var fromUuid) || fromUuid == Guid.Empty)
             {
-                return null;
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/from", [new("from", $"Parameter is not a valid UUID.")]);
             }
 
-            return (ref ValidationErrorBuilder errors) =>
+            if (!Guid.TryParse(to, out var toUuid) || toUuid == Guid.Empty)
             {
-                errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/from", [new("from", "must match the 'party' UUID.")]);
-            };
+                return (ref ValidationErrorBuilder errors) =>
+                    errors.Add(ValidationErrors.InvalidQueryParameter, $"QUERY/to", [new("to", $"Parameter is not a valid UUID.")]);
+            }
+
+            if (partyUuid != fromUuid || partyUuid != toUuid)
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/EnduserValidationRules.cs
user: login: andreasisnes
body: jo stemmer, fiksa
created_at: 2025-05-09T11:27:59Z
author_association: COLLABORATOR
diff_hunk: @@ -22,281 +22,184 @@ namespace Altinn.AccessManagement.Api.Enduser.Controllers;
 [Route("accessmanagement/api/v1/enduser/connections")]
 [FeatureGate(AccessManagementEnduserFeatureFlags.ControllerConnections)]
 [Authorize(Policy = AuthzConstants.SCOPE_PORTAL_ENDUSER)]
-public class ConnectionController(IHttpContextAccessor accessor, IConnectionService connectionService, IAssignmentService assignmentService, IEntityRepository entityRepository) : ControllerBase
+public class ConnectionController(IEnduserConnectionService connectionService) : ControllerBase
 {
-    private IHttpContextAccessor Accessor { get; } = accessor;
+    private IEnduserConnectionService ConnectionService { get; } = connectionService;
 
     /// <summary>
-    /// Creates an assignment between the authenticated user's selected party and the specified target party.
+    /// Get connections between the authenticated user's selected party and the specified target party.
     /// </summary>
-    /// <param name="party">The GUID identifying the party the authenticated user is acting on behalf of.</param>
-    /// <param name="from">The GUID identifying the party the authenticated user is acting for</param>
-    /// <param name="to">The GUID identifying the target party to which the assignment should be created.</param>
-    /// <param name="cancellationToken"><see cref="CancellationToken"/></param>
     [HttpGet]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_READ)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    [ProducesResponseType<AssignmentExternal>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
-    [ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<PaginatedResult<AssignmentExternal>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
     [ProducesResponseType(StatusCodes.Status401Unauthorized)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
-    public async Task<IActionResult> GetConnection([FromQuery] Guid party, [FromQuery] Guid? from, [FromQuery] Guid? to, CancellationToken cancellationToken = default)
+    public async Task<IActionResult> GetConnections([FromQuery] ConnectionInput connection, [FromQuery, FromHeader] PagingInput paging, CancellationToken cancellationToken = default)
     {
-        if (!from.HasValue && !to.HasValue)
-        {
-            return BadRequest();
-        }
-
-        if (!(from.HasValue && from.Value == party) && !(to.HasValue && to.Value == party))
-        {
-            // Party must match From or To
-            return BadRequest();
-        }
-
-        var partyUuid = Accessor.GetPartyUuid();
-        var audit = new ChangeRequestOptions()
-        {
-            ChangedBy = partyUuid,
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from.HasValue && to.HasValue)
+        if (ValidationRules.EnduserReadConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Ok(await connectionService.Get(fromId: from.Value, toId: to.Value, facilitatorId: null, cancellationToken: cancellationToken));
+            return problem.ToActionResult();
         }
 
-        if (from.HasValue)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        var result = await ConnectionService.Get(fromUuid, toUuid, cancellationToken: cancellationToken);
+        if (result.IsProblem)
         {
-            return Ok(await connectionService.GetReceived(from.Value, cancellationToken: cancellationToken));
+            return result.Problem.ToActionResult();
         }
 
-        if (to.HasValue)
-        {
-            return Ok(await connectionService.GetGiven(to.Value, cancellationToken: cancellationToken));
-        }
-        
-        return BadRequest();
+        return Ok(PaginatedResult.Create(result.Value, null));
     }
 
     /// <summary>
     /// Add package to connection (assignment or delegation)
     /// </summary>
     [HttpPost]
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<IActionResult> AddConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, CancellationToken cancellationToken = default)
+    [ProducesResponseType<Assignment>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> AddAssignment([FromQuery] ConnectionInput connection, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (from != party)
-        {
-            throw new Exception("From party does not match from");
-        }
-
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
-        {
-            return Problem("From party not found");
-        }
-
-        if (!fromEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
-        {
-            return Problem("From must be of type 'Organisasjon'");
-        }
-
-        //// To must be Type:Organisasjon
-        //// #550:AC:Det skal bare være mulig å legge til Organisasjoner som ny Rightholder
-        var toEntity = await entityRepository.GetExtended(to);
-        if (toEntity == null)
+        if (ValidationRules.EnduserAddConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            return Problem("To party not found");
+            return problem.ToActionResult();
         }
 
-        if (!toEntity.Type.Name.Equals("Organisasjon", StringComparison.OrdinalIgnoreCase))
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        var result = await ConnectionService.AddAssignment(fromUuid, toUuid, "rettighetshaver", cancellationToken);
+        if (result.IsProblem)
         {
-            return Problem("To must be of type 'Organisasjon'");
+            return result.Problem.ToActionResult();
         }
 
-        var res = await assignmentService.GetOrCreateAssignmentInternal(fromId: from, toId: to, roleCode: "rettighetshaver", options, cancellationToken: cancellationToken);
-
-        if (res != null)
-        {
-            return Ok(res);
-        }
-
-        return Problem("Unable add connection");
+        return Ok(result.Value);
     }
 
     /// <summary>
     /// Remove package from connection (assignment or delegation)
     /// </summary>
     [HttpDelete]
     [Authorize(Policy = AuthzConstants.POLICY_ACCESS_MANAGEMENT_ENDUSER_WRITE)]
-    [ServiceFilter(typeof(AuthorizePartyUuidClaimFilter))]
-    public async Task<ProblemInstance> RemoveConnection([FromQuery] Guid party, [FromQuery] Guid from, [FromQuery] Guid to, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
+    [DbAudit(Claim = AltinnCoreClaimTypes.PartyUuid, System = AuditDefaults.EnduserApiStr)]
+    [ProducesResponseType(StatusCodes.Status204NoContent)]
+    [ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
+    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+    [ProducesResponseType(StatusCodes.Status403Forbidden)]
+    public async Task<IActionResult> RemoveAssignment([FromQuery] ConnectionInput connection, [FromQuery] bool cascade = false, CancellationToken cancellationToken = default)
     {
-        var options = new ChangeRequestOptions()
-        {
-            ChangedBy = Accessor.GetPartyUuid(),
-            ChangedBySystem = AuditDefaults.EnduserApi
-        };
-
-        if (!(from == party) && !(to == party))
+        if (ValidationRules.EnduserRemoveConnection(connection.Party, connection.From, connection.To) is var problem && problem is { })
         {
-            throw new Exception("From party does not match from or to");
+            return problem.ToActionResult();
         }
 
-        //// From must by Type:Organisasjon
-        //// #550:AC:From party må være en organisasjon (skal ikke være mulig å legge til rightholder for privatperson el. andre entitetstyper)
-        var fromEntity = await entityRepository.GetExtended(from);
-        if (fromEntity == null)
+        Guid.TryParse(connection.From, out var fromUuid);
+        Guid.TryParse(connection.To, out var toUuid);
+        problem = await ConnectionService.RemoveAssignment(fromUuid, toUuid, "rettighetshaver", cascade, cancellationToken);
+        if (problem is { })
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Controllers/ConnectionController.cs
user: login: andreasisnes
body: den sjekker om problem er alt annet enn null
created_at: 2025-05-09T12:07:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,416 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using Altinn.AccessManagement.Core.Errors;
+using Altinn.AccessMgmt.Core.Models;
+using Altinn.AccessMgmt.Persistence.Core.Helpers;
+using Altinn.AccessMgmt.Persistence.Repositories.Contracts;
+using Altinn.AccessMgmt.Persistence.Services;
+using Altinn.Authorization.ProblemDetails;
+
+namespace Altinn.AccessManagement.Enduser.Services;
+
+/// <summary>
+/// Service for managing connections.
+/// </summary>
+public class ConnectionService(
+    IDbAudit dbAudit,
+    IConnectionRepository connectionRepository,
+    IConnectionPackageRepository connectionPackageRepository,
+    IPackageRepository packageRepository,
+    IRoleRepository roleRepository,
+    IAssignmentRepository assignmentRepository,
+    IDelegationRepository delegationRepository,
+    IAssignmentPackageRepository assignmentPackageRepository,
+    IEntityRepository entityRepository
+    ) : IEnduserConnectionService
+{
+    private IDbAudit DbAudit { get; } = dbAudit;
+
+    private IConnectionRepository ConnectionRepository { get; } = connectionRepository;
+
+    private IConnectionPackageRepository ConnectionPackageRepository { get; } = connectionPackageRepository;
+
+    private IPackageRepository PackageRepository { get; } = packageRepository;
+
+    private IRoleRepository RoleRepository { get; } = roleRepository;
+
+    private IAssignmentRepository AssignmentRepository { get; } = assignmentRepository;
+
+    private IDelegationRepository DelegationRepository { get; } = delegationRepository;
+
+    private IAssignmentPackageRepository AssignmentPackageRepository { get; } = assignmentPackageRepository;
+
+    private IEntityRepository EntityRepository { get; } = entityRepository;
+
+    /// <inheritdoc />
+    public async Task<Result<List<ExtConnection>>> Get(Guid? from = null, Guid? to = null, CancellationToken cancellationToken = default)
+    {
+        var filter = ConnectionRepository.CreateFilterBuilder();
+        static bool SetFrom(Guid? from, GenericFilterBuilder<Connection> filter)
+        {
+            if (from != null && from.HasValue && from.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.FromId, from);
+                return true;
+            }
+
+            filter.NotSet(t => t.FromId);
+            return false;
+        }
+
+        static bool SetTo(Guid? to, GenericFilterBuilder<Connection> filter)
+        {
+            if (to != null && to.HasValue && to.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.ToId, to);
+                return true;
+            }
+
+            filter.NotSet(t => t.ToId);
+            return false;
+        }
+
+        var isFromSet = SetFrom(from, filter);
+        var isToSet = SetTo(to, filter);
+        if (isFromSet || isToSet)
+        {
+            filter.NotSet(t => t.Id);
+            filter.NotSet(t => t.FacilitatorId);
+        }
+        else
+        {
+            Unreachable();
+        }
+
+        var result = await ConnectionRepository.GetExtended(filter, cancellationToken: cancellationToken);
+        return result?.ToList() ?? [];
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<Assignment>> AddAssignment(Guid from, Guid to, string roleCode, CancellationToken cancellationToken = default)
+    {
+        var dependencies = await GetAssignmentDependencies(from, to, roleCode, cancellationToken);
+        if (ValidateAssignmentData(dependencies.EntityFrom, dependencies.EntityTo, dependencies.Roles) is var problem && problem is { })
+        {
+            return problem;
+        }
+
+        var roleId = dependencies.Roles.First().Id;
+        var filter = AssignmentRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.ToId, to)
+            .Equal(t => t.RoleId, roleId);
+
+        var existingAssignment = await AssignmentRepository.Get(filter, cancellationToken: cancellationToken);
+        if (existingAssignment != null && existingAssignment.Any())
+        {
+            return existingAssignment.First();
+        }
+
+        var assignment = new Assignment()
+        {
+            FromId = from,
+            ToId = to,
+            RoleId = roleId,
+        };
+
+        var result = await AssignmentRepository.Create(assignment, DbAudit.Value, cancellationToken);
+        if (result == 0)
+        {
+            Unreachable();
+        }
+
+        return assignment;
+    }
+
+    /// <inheritdoc />
+    public async Task<ValidationProblemInstance> RemoveAssignment(Guid from, Guid to, string roleCode, bool cascade, CancellationToken cancellationToken = default)
+    {
+        var dependencies = await GetAssignmentDependencies(from, to, roleCode, cancellationToken);
+        if (ValidateAssignmentData(dependencies.EntityFrom, dependencies.EntityTo, dependencies.Roles) is var problem && problem is { })
+        {
+            return problem;
+        }
+
+        var roleId = dependencies.Roles.First().Id;
+        var filter = AssignmentRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.ToId, to)
+            .Equal(t => t.RoleId, roleId);
+
+        var existingAssignments = await AssignmentRepository.Get(filter, cancellationToken: cancellationToken);
+        if (existingAssignments != null && !existingAssignments.Any())
+        {
+            return null;
+        }
+
+        var existingAssignment = existingAssignments.First();
+
+        if (!cascade)
+        {
+            var packages = await AssignmentPackageRepository.Get(f => f.AssignmentId, existingAssignment.Id, cancellationToken: cancellationToken);
+            var delegationsFrom = await DelegationRepository.Get(f => f.FromId, existingAssignment.Id, cancellationToken: cancellationToken);
+            var delegationsTo = await DelegationRepository.Get(f => f.ToId, existingAssignment.Id, cancellationToken: cancellationToken);
+
+            problem = ValidationRules.Validate(
+                ValidationRules.QueryParameters.HasPackagesAssigned(packages),
+                ValidationRules.QueryParameters.HasDelegationsAssigned(delegationsFrom),
+                ValidationRules.QueryParameters.HasDelegationsAssigned(delegationsTo)
+            );
+            if (problem is { })
+            {
+                return problem;
+            }
+        }
+
+        var result = await AssignmentRepository.Delete(existingAssignment.Id, DbAudit.Value, cancellationToken: cancellationToken);
+        if (result == 0)
+        {
+            Unreachable();
+        }
+
+        return null;
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<List<ConnectionPackage>>> GetPackages(Guid? from, Guid? to, CancellationToken cancellationToken = default)
+    {
+        var filter = ConnectionPackageRepository.CreateFilterBuilder();
+        static bool SetFrom(Guid? from, GenericFilterBuilder<ConnectionPackage> filter)
+        {
+            if (from != null && from.HasValue && from.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.FromId, from);
+                return true;
+            }
+
+            filter.NotSet(t => t.FromId);
+            return false;
+        }
+
+        static bool SetTo(Guid? to, GenericFilterBuilder<ConnectionPackage> filter)
+        {
+            if (to != null && to.HasValue && to.Value != Guid.Empty)
+            {
+                filter.Equal(t => t.ToId, to);
+                return true;
+            }
+
+            filter.NotSet(t => t.ToId);
+            return false;
+        }
+
+        var isFromSet = SetFrom(from, filter);
+        var isToSet = SetTo(to, filter);
+        if (isFromSet || isToSet)
+        {
+            filter.NotSet(t => t.Id);
+            filter.NotSet(t => t.FacilitatorId);
+        }
+        else
+        {
+            Unreachable();
+        }
+
+        var result = await ConnectionPackageRepository.Get(filter, cancellationToken: cancellationToken);
+        return result.ToList();
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<AssignmentPackage>> AddPackage(Guid from, Guid to, string roleCode, string packageUrn, CancellationToken cancellationToken = default)
+    {
+        var filter = PackageRepository.CreateFilterBuilder()
+            .Add(t => t.Urn, packageUrn, FilterComparer.EndsWith);
+
+        var packages = await PackageRepository.Get(filter, cancellationToken: cancellationToken);
+        var problem = ValidationRules.Validate(ValidationRules.QueryParameters.PackageUrnLookup(packages));
+        if (problem is { })
+        {
+            return problem;
+        }
+
+        var package = packages.First();
+        return await AddPackage(from, to, roleCode, package.Id, "packageUrn", cancellationToken);
+    }
+
+    /// <inheritdoc />
+    public async Task<Result<AssignmentPackage>> AddPackage(Guid from, Guid to, string roleCode, Guid packageId, CancellationToken cancellationToken = default)
+    {
+        return await AddPackage(from, to, roleCode, packageId, "packageId", cancellationToken);
+    }
+
+    private async Task<Result<AssignmentPackage>> AddPackage(Guid from, Guid to, string roleCode, Guid packageId, string queryParamName, CancellationToken cancellationToken)
+    {
+        var dependencies = await GetAssignmentDependencies(from, to, roleCode, cancellationToken);
+        if (ValidateAssignmentData(dependencies.EntityFrom, dependencies.EntityTo, dependencies.Roles) is var problem && problem is { })
+        {
+            return problem;
+        }
+
+        var roleId = dependencies.Roles.First().Id;
+        var filter = AssignmentRepository.CreateFilterBuilder()
+            .Equal(t => t.FromId, from)
+            .Equal(t => t.ToId, to)
+            .Equal(t => t.RoleId, roleId);
+
+        var existingAssignments = await AssignmentRepository.Get(filter, cancellationToken: cancellationToken);
+        problem = ValidationRules.Validate(ValidationRules.QueryParameters.VerifyAssignmentRoleExists(existingAssignments, roleCode));
+        if (problem is { })
+        {
+            return problem;
+        }
+
+        var assignment = existingAssignments.First();
+
+        var userPackageFilter = ConnectionPackageRepository.CreateFilterBuilder()
+            .Equal(t => t.ToId, from)
path: src/apps/Altinn.AccessManagement/src/Altinn.AccessManagement.Api.Enduser/Services/ConnectionService.cs
user: login: jonkjetiloye
body: ```
.Equal(t => t.FromId, from)
.Equal(t => t.ToId, DbAudit.Value.ChangedBy)
.Equal(t => t.PackageId, packageId);
```
created_at: 2025-05-09T14:32:52Z
author_association: MEMBER

PR comments:
user: login: andreasisnes
created_at: 2025-05-08T16:31:58Z
body: ignorer at auth er kommentert ut enn så lenge. må fjerne den for testing
author_association: COLLABORATOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-13T09:32:22Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=Authorization_AccessManagement&pullRequest=684) **Quality Gate failed for 'Authorization Altinn.AccessManagement'**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [4.2% Coverage on New Code](https://sonarcloud.io/component_measures?id=Authorization_AccessManagement&pullRequest=684&metric=new_coverage&view=list) (required ≥ 65%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=Authorization_AccessManagement&pullRequest=684)


author_association: NONE