Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System.Net.Mime;
using Altinn.AccessManagement.Core.Configuration;
using Altinn.AccessManagement.Core.Constants;
using Altinn.AccessManagement.Core.Errors;
using Altinn.AccessMgmt.Core.Audit;
using Altinn.AccessMgmt.Core.Services;
using Altinn.AccessMgmt.Core.Services.Contracts;
using Altinn.AccessMgmt.Core.Utils;
using Altinn.AccessMgmt.PersistenceEF.Constants;
using Altinn.AccessMgmt.PersistenceEF.Models;
using Altinn.Authorization.Api.Contracts.AccessManagement;
using Altinn.Authorization.Api.Contracts.Register;
using Altinn.Authorization.ProblemDetails;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

namespace Altinn.AccessManagement.Api.ServiceOwner.Controllers
{
/// <summary>
/// Connection service for service owner
/// </summary>
[ApiController]
[Route("accessmanagement/api/v1/serviceowner/connections")]
public class ConnectionsController(
IConnectionServiceServiceOwner connectionService,
IEntityService EntityService,
IPackageService packageService,
IOptions<ServiceOwnerDelegationSettings> serviceOwnerDelegationSettings
) : ControllerBase
{
private Action<ConnectionOptions> ConfigureConnections { get; } = options =>
{
options.AllowedWriteFromEntityTypes = [EntityTypeConstants.Organization, EntityTypeConstants.Person];
options.AllowedWriteToEntityTypes = [EntityTypeConstants.Organization, EntityTypeConstants.Person];
options.AllowedReadFromEntityTypes = [EntityTypeConstants.Organization, EntityTypeConstants.Person];
options.AllowedReadToEntityTypes = [EntityTypeConstants.Organization, EntityTypeConstants.Person];
options.FilterFromEntityTypes = [];
options.FilterToEntityTypes = [];
};

[HttpPost("accesspackages")]
[AuditServiceOwnerConsumer]
[Authorize(Policy = AuthzConstants.SCOPE_SERVICEOWNER_PACKAGE_WRITE)]
[ProducesResponseType<AssignmentPackageDto>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
[ProducesResponseType<AltinnProblemDetails>(StatusCodes.Status400BadRequest, MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> AddPackages([FromBody] ServiceOwnerAccessPackageDelegation packageDelegation, CancellationToken cancellationToken = default)
{
// Validate service owner is authorized to delegate this package
string packageIdentifier = packageDelegation.PackageUrn.ValueSpan.ToString();
if (!IsServiceOwnerAuthorizedForPackage(packageIdentifier))
{
return Problems.PackageDelegationNotAuthorized.ToActionResult();
}

Guid? fromEntity = null;
Guid? toEntity = null;

if (packageDelegation.From.IsPersonId(out PersonIdentifier person))
{
Entity personFrom = await EntityService.GetByPersNo(person.ToString(), cancellationToken);
fromEntity = personFrom?.Id;
}

if (packageDelegation.To.IsPersonId(out PersonIdentifier personTo))
{
Entity personToEntity = await EntityService.GetByPersNo(personTo.ToString(), cancellationToken);
toEntity = personToEntity?.Id;
}

// Validate entities exist
if (fromEntity is null || toEntity is null)
{
return Problems.ConnectionEntitiesDoNotExist.ToActionResult();
}

PackageDto package = await packageService.GetPackageByUrnValue(packageIdentifier, cancellationToken);

if (package is null)
{
return Problems.PackageNotFound.ToActionResult();
}

Result<AssignmentPackageDto> result = await connectionService.AddPackage(fromEntity.Value, toEntity.Value, package.Id, ConfigureConnections, cancellationToken);

if (result.IsProblem)
{
return result.Problem.ToActionResult();
}

return Ok(result.Value);
}

private bool IsServiceOwnerAuthorizedForPackage(string packageIdentifier)
{
var consumerParty = OrgUtil.GetAuthenticatedParty(User);
if (consumerParty is null || !consumerParty.IsOrganizationId(out var organizationNumber))
{
return false;
}

var whiteList = serviceOwnerDelegationSettings.Value.PackageWhiteList;
if (whiteList.TryGetValue(organizationNumber.ToString(), out var allowedPackages))
{
return allowedPackages.Contains(packageIdentifier, StringComparer.OrdinalIgnoreCase);
}

return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Altinn.AccessManagement.Core.Configuration;

/// <summary>
/// Configuration settings for resource owner delegation package whitelist
/// </summary>
public class ServiceOwnerDelegationSettings
{
/// <summary>
/// Gets or sets the whitelist of access packages that service owners are authorized to delegate.
/// Key is the organization number (from consumer claim in Maskinporten).
/// Value is the list of access package identifiers that the service owner is authorized to delegate.
/// </summary>
public Dictionary<string, List<string>> PackageWhiteList { get; set; } = [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ public static class AuthzConstants
/// </summary>
public const string SCOPE_CONSENTREQUEST_READ = "altinn:consentrequests.read";

/// <summary>
/// Scope giving service owners access to delegate access packages betweeen two parties. Limited to packages "owned" by the service owner. Very limited access to this scope
/// </summary>
public const string SCOPE_SERVICEOWNER_PACKAGE_WRITE = "altinn:serviceowner/package.write";

/// <summary>
/// Claim for scopes from maskinporten token
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ private static void ConfigureAuthorization(this WebApplicationBuilder builder)
.AddPolicy(AuthzConstants.ALTINN_SERVICEOWNER_DELEGATIONREQUESTS_READ, policy => policy.Requirements.Add(new ScopeAccessRequirement([AuthzConstants.ALTINN_SERVICEOWNER_DELEGATIONREQUESTS_READ])))
.AddPolicy(AuthzConstants.ALTINN_SERVICEOWNER_DELEGATIONREQUESTS_WRITE, policy => policy.Requirements.Add(new ScopeAccessRequirement([AuthzConstants.ALTINN_SERVICEOWNER_DELEGATIONREQUESTS_WRITE])))
.AddPolicy(AuthzConstants.SCOPE_PORTAL_ENDUSER, policy => policy.Requirements.Add(new ScopeAccessRequirement([AuthzConstants.SCOPE_PORTAL_ENDUSER])))
.AddPolicy(AuthzConstants.SCOPE_SERVICEOWNER_PACKAGE_WRITE, policy => policy.Requirements.Add(new ScopeAccessRequirement([AuthzConstants.SCOPE_SERVICEOWNER_PACKAGE_WRITE])))
.AddPolicy(AuthzConstants.POLICY_ENDUSER_CONNECTIONS_BIDRECTIONAL_READ, policy => policy.AddRequirementConditionalScope(
new ConditionalScope(ConditionalScope.FromOthers, AuthzConstants.SCOPE_PORTAL_ENDUSER, AuthzConstants.SCOPE_ENDUSER_CONNECTIONS_FROMOTHERS_READ),
new ConditionalScope(ConditionalScope.ToOthers, AuthzConstants.SCOPE_PORTAL_ENDUSER, AuthzConstants.SCOPE_ENDUSER_CONNECTIONS_TOOTHERS_READ)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@
"EmptyFeedDelayMs": 60000,
"FeatureDisabledDelayMs": 600000
},
"ServiceOwnerDelegation": {
"PackageWhiteList": {
"974761076": [ "innbygger-skatteforhold-privatpersoner" ]
}
},
"AppsInstanceDelegationSettings": {
"MaxPolicyFilesToRevoke": 10
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ public class AuditMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var auditContextAccessor = context.RequestServices.GetRequiredService<IAuditAccessor>();
IAuditAccessor auditContextAccessor = context.RequestServices.GetRequiredService<IAuditAccessor>();
if (context.GetEndpoint() is var endpoint && endpoint != null)
{
if (endpoint.Metadata.GetMetadata<AuditJWTClaimToDbAttribute>() is var jwtClaimToDb && jwtClaimToDb != null)
{
var claim = context.User?.Claims?
Claim claim = context.User?.Claims?
.FirstOrDefault(c => c.Type.Equals(jwtClaimToDb.Claim, StringComparison.OrdinalIgnoreCase));

if (claim == null && jwtClaimToDb.AllowSystemUser)
Expand All @@ -49,12 +49,12 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next)
}
else if (endpoint.Metadata.GetMetadata<AuditServiceOwnerConsumerAttribute>() is var serviceOwnerConsumer && serviceOwnerConsumer is { })
{
var party = OrgUtil.GetAuthenticatedParty(context.User);
if (party is { })
ConsentPartyUrn party = OrgUtil.GetAuthenticatedParty(context.User);
if (party is not null)
{
var db = context.RequestServices.GetRequiredService<AppDbContext>();
var entity = await GetEntityFromConsumerClaim(db, context, party);
if (entity is { })
AppDbContext db = context.RequestServices.GetRequiredService<AppDbContext>();
Entity entity = await GetEntityFromConsumerClaim(db, context, party);
if (entity is not null)
{
auditContextAccessor.AuditValues = new(entity.Id, SystemEntityConstants.ServiceOwnerApi, TraceId(context));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public static IServiceCollection AddAccessMgmtCore(this IServiceCollection servi
services.AddScoped<IAuthorizedPartyRepoServiceEf, AuthorizedPartyRepoServiceEf>();
services.AddScoped<IClientDelegationService, ClientDelegationService>();
services.AddScoped<IRequestService, RequestService>();
services.AddScoped<IConnectionServiceServiceOwner, ConnectionServiceServiceOwner>();
services.AddScoped<IAuthorizedPartiesService, AuthorizedPartiesServiceEf>();

services.AddScoped<IAuthorizationScopeProvider, DefaultAuthorizationScopeProvider>();
Expand All @@ -55,8 +56,12 @@ public static IServiceCollection AddAccessMgmtCore(this IServiceCollection servi
.ValidateOnStart()
.BindConfiguration("ConsentMigration");

// Resource Owner Delegation - Configuration
services.AddOptions<ServiceOwnerDelegationSettings>()
.BindConfiguration("ServiceOwnerDelegation");

// Consent Migration - Services (Core - Scoped)
services.AddScoped<IConsentMigrationService, ConsentMigrationService>();
services.AddScoped<IConsentMigrationService, ConsentMigrationService>();

AddJobs(services);
return services;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Altinn.AccessMgmt.Core.Services.Contracts;
using Altinn.AccessMgmt.Core.Utils;
using Altinn.AccessMgmt.PersistenceEF.Constants;
using Altinn.AccessMgmt.PersistenceEF.Contexts;
using Altinn.AccessMgmt.PersistenceEF.Models;
using Altinn.Authorization.Api.Contracts.AccessManagement;
using Altinn.Authorization.ProblemDetails;
using Microsoft.EntityFrameworkCore;

namespace Altinn.AccessMgmt.Core.Services
{
public class ConnectionServiceServiceOwner(
AppDbContext dbContext) : IConnectionServiceServiceOwner
{
/// <summary>
/// Allows service owners to
/// </summary>
public async Task<Result<AssignmentPackageDto>> AddPackage(Guid fromId, Guid toId, Guid packageId, Action<ConnectionOptions> configureConnection = null, CancellationToken cancellationToken = default)
{
ConnectionOptions options = new(configureConnection);

Check warning on line 20 in src/apps/Altinn.AccessManagement/src/Altinn.AccessMgmt.Core/Services/ConnectionServiceServiceOwner.cs

View check run for this annotation

SonarQubeCloud / [Authorization Altinn.AccessManagement] SonarCloud Code Analysis

Remove the unused local variable 'options'.

See more on https://sonarcloud.io/project/issues?id=Authorization_AccessManagement&issues=AZzZMjYpAEk4RYmk7epI&open=AZzZMjYpAEk4RYmk7epI&pullRequest=2514

// Look for existing direct rightholder assignment
Assignment assignment = await dbContext.Assignments
.Where(a => a.FromId == fromId)
.Where(a => a.ToId == toId)
.Where(a => a.RoleId == RoleConstants.Rightholder.Id)
.FirstOrDefaultAsync(cancellationToken);

if (assignment == null)
{
assignment = new Assignment()
{
FromId = fromId,
ToId = toId,
RoleId = RoleConstants.Rightholder
};

await dbContext.Assignments.AddAsync(assignment, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken); // Save to get the ID
}

// Check if package already assigned
AssignmentPackage existingAssignmentPackage = await dbContext.AssignmentPackages
.AsNoTracking()
.Where(a => a.AssignmentId == assignment.Id)
.Where(a => a.PackageId == packageId)
.FirstOrDefaultAsync(cancellationToken);

if (existingAssignmentPackage is { })
{
return DtoMapper.Convert(existingAssignmentPackage);
}

var newAssignmentPackage = new AssignmentPackage()
{
AssignmentId = assignment.Id,
PackageId = packageId,
};

await dbContext.AssignmentPackages.AddAsync(newAssignmentPackage, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);

return DtoMapper.Convert(newAssignmentPackage);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Altinn.Authorization.Api.Contracts.AccessManagement;
using Altinn.Authorization.ProblemDetails;

namespace Altinn.AccessMgmt.Core.Services.Contracts
{
public interface IConnectionServiceServiceOwner
{
Task<Result<AssignmentPackageDto>> AddPackage(Guid fromId, Guid toId, Guid packageId, Action<ConnectionOptions> configureConnection = null, CancellationToken cancellationToken = default);
}
}
Loading
Loading