diff --git a/backend/packagegroups/NuGet.props b/backend/packagegroups/NuGet.props
index 5373de494f5..92bc5c44d6e 100644
--- a/backend/packagegroups/NuGet.props
+++ b/backend/packagegroups/NuGet.props
@@ -50,14 +50,15 @@
-
+
-
-
+
+
+
diff --git a/backend/src/Designer/Controllers/AppScopesController.cs b/backend/src/Designer/Controllers/AppScopesController.cs
new file mode 100644
index 00000000000..f3c699e9244
--- /dev/null
+++ b/backend/src/Designer/Controllers/AppScopesController.cs
@@ -0,0 +1,77 @@
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Altinn.Studio.Designer.Constants;
+using Altinn.Studio.Designer.Helpers;
+using Altinn.Studio.Designer.Models;
+using Altinn.Studio.Designer.Models.Dto;
+using Altinn.Studio.Designer.Repository.Models.AppScope;
+using Altinn.Studio.Designer.Services.Interfaces;
+using Altinn.Studio.Designer.TypedHttpClients.MaskinPorten;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.FeatureManagement.Mvc;
+
+
+namespace Altinn.Studio.Designer.Controllers;
+
+[FeatureGate(StudioFeatureFlags.AnsattPorten)]
+[Route("designer/api/{org}/{app:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/app-scopes")]
+
+public class AppScopesController(IMaskinPortenHttpClient maskinPortenHttpClient,
+ IAppScopesService appScopesService) : ControllerBase
+{
+ [Authorize(AnsattPortenConstants.AnsattportenAuthorizationPolicy)]
+ [HttpGet("maskinporten")]
+ public async Task GetScopesFromMaskinPorten(string org, string app, CancellationToken cancellationToken)
+ {
+ var scopes = await maskinPortenHttpClient.GetAvailableScopes(cancellationToken);
+
+ var reponse = new AppScopesResponse()
+ {
+ Scopes = scopes.Select(x => new MaskinPortenScopeDto()
+ {
+ Scope = x.Scope,
+ Description = x.Description
+ }).ToHashSet()
+ };
+
+ return Ok(reponse);
+ }
+
+
+ [Authorize]
+ [HttpPut]
+ public async Task UpsertAppScopes(string org, string app, [FromBody] AppScopesUpsertRequest appScopesUpsertRequest,
+ CancellationToken cancellationToken)
+ {
+ var scopes = appScopesUpsertRequest.Scopes.Select(x => new MaskinPortenScopeEntity()
+ {
+ Scope = x.Scope,
+ Description = x.Description
+ }).ToHashSet();
+
+ string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
+ await appScopesService.UpsertScopesAsync(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer), scopes, cancellationToken);
+ }
+
+
+ [Authorize]
+ [HttpGet]
+ public async Task GetAppScopes(string org, string app, CancellationToken cancellationToken)
+ {
+ var appScopes = await appScopesService.GetAppScopesAsync(AltinnRepoContext.FromOrgRepo(org, app), cancellationToken);
+
+ var reponse = new AppScopesResponse()
+ {
+ Scopes = appScopes?.Scopes.Select(x => new MaskinPortenScopeDto()
+ {
+ Scope = x.Scope,
+ Description = x.Description
+ }).ToHashSet() ?? []
+ };
+
+ return Ok(reponse);
+ }
+
+}
diff --git a/backend/src/Designer/Controllers/MaskinPortenController.cs b/backend/src/Designer/Controllers/MaskinPortenController.cs
deleted file mode 100644
index 2c0e1ebae94..00000000000
--- a/backend/src/Designer/Controllers/MaskinPortenController.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-using Altinn.Studio.Designer.Constants;
-using Altinn.Studio.Designer.TypedHttpClients.MaskinPorten;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.FeatureManagement.Mvc;
-
-
-namespace Altinn.Studio.Designer.Controllers;
-
-[FeatureGate(StudioFeatureFlags.AnsattPorten)]
-[Route("designer/api/[controller]")]
-public class MaskinPortenController(IMaskinPortenHttpClient maskinPortenHttpClient) : ControllerBase
-{
-
- // TODO: Cleanup model and create separation between presentation dto, domain model and external api model
- // Will be done under: https://github.com/Altinn/altinn-studio/issues/12767 and https://github.com/Altinn/altinn-studio/issues/12766
- [Authorize(AnsattPortenConstants.AnsattportenAuthorizationPolicy)]
- [HttpGet("scopes")]
- public async Task Get(CancellationToken cancellationToken)
- {
- var scopes = await maskinPortenHttpClient.GetAvailableScopes(cancellationToken);
- return Ok(scopes);
- }
-}
diff --git a/backend/src/Designer/Infrastructure/AnsattPorten/AnsattPortenExtensions.cs b/backend/src/Designer/Infrastructure/AnsattPorten/AnsattPortenExtensions.cs
index 1f76ad1f87b..8cfdfcc3881 100644
--- a/backend/src/Designer/Infrastructure/AnsattPorten/AnsattPortenExtensions.cs
+++ b/backend/src/Designer/Infrastructure/AnsattPorten/AnsattPortenExtensions.cs
@@ -75,7 +75,8 @@ private static IServiceCollection AddAnsattPortenAuthentication(this IServiceCol
options.Events.OnRedirectToIdentityProvider = context =>
{
- if (!context.Request.Path.StartsWithSegments("/designer/api/maskinporten"))
+ if (!context.Request.Path.StartsWithSegments("/designer/api") ||
+ !context.Request.Path.Value!.Contains("/maskinporten"))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.HandleResponse();
diff --git a/backend/src/Designer/Infrastructure/ServiceRegistration.cs b/backend/src/Designer/Infrastructure/ServiceRegistration.cs
index 4218ad0431f..e85ecd0f2bf 100644
--- a/backend/src/Designer/Infrastructure/ServiceRegistration.cs
+++ b/backend/src/Designer/Infrastructure/ServiceRegistration.cs
@@ -56,8 +56,10 @@ public static IServiceCollection RegisterServiceImplementations(this IServiceCol
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
services.AddTransient();
services.AddTransient();
services.AddTransient();
diff --git a/backend/src/Designer/Migrations/20240919171846_AppScopesTable.Designer.cs b/backend/src/Designer/Migrations/20240919171846_AppScopesTable.Designer.cs
new file mode 100644
index 00000000000..518bfcaf341
--- /dev/null
+++ b/backend/src/Designer/Migrations/20240919171846_AppScopesTable.Designer.cs
@@ -0,0 +1,169 @@
+//
+using System;
+using Altinn.Studio.Designer.Repository.ORMImplementation.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Altinn.Studio.Designer.Migrations
+{
+ [DbContext(typeof(DesignerdbContext))]
+ [Migration("20240919171846_AppScopesTable")]
+ partial class AppScopesTable
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.8")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseSerialColumns(modelBuilder);
+
+ modelBuilder.Entity("Altinn.Studio.Designer.Repository.ORMImplementation.Models.AppScopesDbObject", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("App")
+ .IsRequired()
+ .HasColumnType("character varying")
+ .HasColumnName("app");
+
+ b.Property("Created")
+ .HasColumnType("timestamptz")
+ .HasColumnName("created");
+
+ b.Property("CreatedBy")
+ .HasColumnType("character varying")
+ .HasColumnName("created_by");
+
+ b.Property("LastModifiedBy")
+ .HasColumnType("character varying")
+ .HasColumnName("last_modified_by");
+
+ b.Property("Org")
+ .IsRequired()
+ .HasColumnType("character varying")
+ .HasColumnName("org");
+
+ b.Property("Scopes")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("scopes");
+
+ b.HasKey("Id")
+ .HasName("app_scopes_pkey");
+
+ b.HasIndex(new[] { "Org", "App" }, "idx_app_scopes_org_app")
+ .IsUnique();
+
+ b.ToTable("app_scopes", "designer");
+ });
+
+ modelBuilder.Entity("Altinn.Studio.Designer.Repository.ORMImplementation.Models.Deployment", b =>
+ {
+ b.Property("Sequenceno")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("BIGSERIAL")
+ .HasColumnName("sequenceno");
+
+ NpgsqlPropertyBuilderExtensions.UseSerialColumn(b.Property("Sequenceno"));
+
+ b.Property("App")
+ .HasColumnType("character varying")
+ .HasColumnName("app");
+
+ b.Property("Buildid")
+ .HasColumnType("character varying")
+ .HasColumnName("buildid");
+
+ b.Property("Buildresult")
+ .HasColumnType("character varying")
+ .HasColumnName("buildresult");
+
+ b.Property("Created")
+ .HasColumnType("timestamptz")
+ .HasColumnName("created");
+
+ b.Property("Entity")
+ .HasColumnType("text")
+ .HasColumnName("entity");
+
+ b.Property("Org")
+ .HasColumnType("character varying")
+ .HasColumnName("org");
+
+ b.Property("Tagname")
+ .HasColumnType("character varying")
+ .HasColumnName("tagname");
+
+ b.HasKey("Sequenceno")
+ .HasName("deployments_pkey");
+
+ b.HasIndex(new[] { "Org", "App" }, "idx_deployments_org_app");
+
+ b.ToTable("deployments", "designer");
+ });
+
+ modelBuilder.Entity("Altinn.Studio.Designer.Repository.ORMImplementation.Models.Release", b =>
+ {
+ b.Property("Sequenceno")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("BIGSERIAL")
+ .HasColumnName("sequenceno");
+
+ NpgsqlPropertyBuilderExtensions.UseSerialColumn(b.Property("Sequenceno"));
+
+ b.Property("App")
+ .HasColumnType("character varying")
+ .HasColumnName("app");
+
+ b.Property("Buildid")
+ .HasColumnType("character varying")
+ .HasColumnName("buildid");
+
+ b.Property("Buildresult")
+ .HasColumnType("character varying")
+ .HasColumnName("buildresult");
+
+ b.Property("Buildstatus")
+ .HasColumnType("character varying")
+ .HasColumnName("buildstatus");
+
+ b.Property("Created")
+ .HasColumnType("timestamptz")
+ .HasColumnName("created");
+
+ b.Property("Entity")
+ .HasColumnType("text")
+ .HasColumnName("entity");
+
+ b.Property("Org")
+ .HasColumnType("character varying")
+ .HasColumnName("org");
+
+ b.Property("Tagname")
+ .HasColumnType("character varying")
+ .HasColumnName("tagname");
+
+ b.HasKey("Sequenceno")
+ .HasName("releases_pkey");
+
+ b.HasIndex(new[] { "Org", "App" }, "idx_releases_org_app");
+
+ b.ToTable("releases", "designer");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/backend/src/Designer/Migrations/20240919171846_AppScopesTable.cs b/backend/src/Designer/Migrations/20240919171846_AppScopesTable.cs
new file mode 100644
index 00000000000..056b570c843
--- /dev/null
+++ b/backend/src/Designer/Migrations/20240919171846_AppScopesTable.cs
@@ -0,0 +1,50 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Altinn.Studio.Designer.Migrations
+{
+ ///
+ public partial class AppScopesTable : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "app_scopes",
+ schema: "designer",
+ columns: table => new
+ {
+ id = table.Column(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ app = table.Column(type: "character varying", nullable: false),
+ org = table.Column(type: "character varying", nullable: false),
+ created = table.Column(type: "timestamptz", nullable: false),
+ scopes = table.Column(type: "jsonb", nullable: false),
+ created_by = table.Column(type: "character varying", nullable: true),
+ last_modified_by = table.Column(type: "character varying", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("app_scopes_pkey", x => x.id);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "idx_app_scopes_org_app",
+ schema: "designer",
+ table: "app_scopes",
+ columns: new[] { "org", "app" },
+ unique: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "app_scopes",
+ schema: "designer");
+ }
+ }
+}
diff --git a/backend/src/Designer/Migrations/DesignerdbContextModelSnapshot.cs b/backend/src/Designer/Migrations/DesignerdbContextModelSnapshot.cs
index c2ef1351c0e..60df1681b56 100644
--- a/backend/src/Designer/Migrations/DesignerdbContextModelSnapshot.cs
+++ b/backend/src/Designer/Migrations/DesignerdbContextModelSnapshot.cs
@@ -17,11 +17,56 @@ protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
- .HasAnnotation("ProductVersion", "8.0.2")
+ .HasAnnotation("ProductVersion", "8.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseSerialColumns(modelBuilder);
+ modelBuilder.Entity("Altinn.Studio.Designer.Repository.ORMImplementation.Models.AppScopesDbObject", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("App")
+ .IsRequired()
+ .HasColumnType("character varying")
+ .HasColumnName("app");
+
+ b.Property("Created")
+ .HasColumnType("timestamptz")
+ .HasColumnName("created");
+
+ b.Property("CreatedBy")
+ .HasColumnType("character varying")
+ .HasColumnName("created_by");
+
+ b.Property("LastModifiedBy")
+ .HasColumnType("character varying")
+ .HasColumnName("last_modified_by");
+
+ b.Property("Org")
+ .IsRequired()
+ .HasColumnType("character varying")
+ .HasColumnName("org");
+
+ b.Property("Scopes")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("scopes");
+
+ b.HasKey("Id")
+ .HasName("app_scopes_pkey");
+
+ b.HasIndex(new[] { "Org", "App" }, "idx_app_scopes_org_app")
+ .IsUnique();
+
+ b.ToTable("app_scopes", "designer");
+ });
+
modelBuilder.Entity("Altinn.Studio.Designer.Repository.ORMImplementation.Models.Deployment", b =>
{
b.Property("Sequenceno")
diff --git a/backend/src/Designer/Models/Dto/AppScopesRequest.cs b/backend/src/Designer/Models/Dto/AppScopesRequest.cs
new file mode 100644
index 00000000000..406ad8247f1
--- /dev/null
+++ b/backend/src/Designer/Models/Dto/AppScopesRequest.cs
@@ -0,0 +1,8 @@
+using System.Collections.Generic;
+
+namespace Altinn.Studio.Designer.Models.Dto;
+
+public record AppScopesUpsertRequest
+{
+ public ISet Scopes { get; set; }
+}
diff --git a/backend/src/Designer/Models/Dto/AppScopesResponse.cs b/backend/src/Designer/Models/Dto/AppScopesResponse.cs
new file mode 100644
index 00000000000..4d1c0e80af3
--- /dev/null
+++ b/backend/src/Designer/Models/Dto/AppScopesResponse.cs
@@ -0,0 +1,8 @@
+using System.Collections.Generic;
+
+namespace Altinn.Studio.Designer.Models.Dto;
+
+public record AppScopesResponse
+{
+ public ISet Scopes { get; set; }
+}
diff --git a/backend/src/Designer/Models/Dto/MaskinPortenScopeDto.cs b/backend/src/Designer/Models/Dto/MaskinPortenScopeDto.cs
new file mode 100644
index 00000000000..26df67d02a5
--- /dev/null
+++ b/backend/src/Designer/Models/Dto/MaskinPortenScopeDto.cs
@@ -0,0 +1,7 @@
+namespace Altinn.Studio.Designer.Models.Dto;
+
+public record MaskinPortenScopeDto
+{
+ public string Scope { get; set; }
+ public string Description { get; set; }
+}
diff --git a/backend/src/Designer/Program.cs b/backend/src/Designer/Program.cs
index 956a9416685..bd8c6a9dbb8 100644
--- a/backend/src/Designer/Program.cs
+++ b/backend/src/Designer/Program.cs
@@ -212,8 +212,8 @@ void ConfigureServices(IServiceCollection services, IConfiguration configuration
services.ConfigureNonMarkedSettings(configuration);
services.RegisterTypedHttpClients(configuration);
- services.ConfigureAuthentication(configuration, env);
services.AddAnsattPortenAuthenticationAndAuthorization(configuration);
+ services.ConfigureAuthentication(configuration, env);
services.Configure(configuration.GetSection("CacheSettings"));
diff --git a/backend/src/Designer/Repository/IAppScopesRepository.cs b/backend/src/Designer/Repository/IAppScopesRepository.cs
new file mode 100644
index 00000000000..94b78f6581c
--- /dev/null
+++ b/backend/src/Designer/Repository/IAppScopesRepository.cs
@@ -0,0 +1,12 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Altinn.Studio.Designer.Models;
+using Altinn.Studio.Designer.Repository.Models.AppScope;
+
+namespace Altinn.Studio.Designer.Repository;
+
+public interface IAppScopesRepository
+{
+ Task GetAppScopesAsync(AltinnRepoContext repoContext, CancellationToken cancellationToken = default);
+ Task UpsertAppScopesAsync(AppScopesEntity appScopesEntity, CancellationToken cancellationToken = default);
+}
diff --git a/backend/src/Designer/Repository/Models/AppScope/AppScopesEntity.cs b/backend/src/Designer/Repository/Models/AppScope/AppScopesEntity.cs
new file mode 100644
index 00000000000..e2dcc80557b
--- /dev/null
+++ b/backend/src/Designer/Repository/Models/AppScope/AppScopesEntity.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+
+namespace Altinn.Studio.Designer.Repository.Models.AppScope;
+
+public class AppScopesEntity
+{
+ public ISet Scopes { get; set; }
+ public DateTimeOffset Created { get; set; }
+ public string CreatedBy { get; set; }
+ public string App { get; set; }
+ public string Org { get; set; }
+ public string LastModifiedBy { get; set; }
+ public uint Version { get; set; }
+}
+
diff --git a/backend/src/Designer/Repository/Models/AppScope/MaskinPortenScopeEntity.cs b/backend/src/Designer/Repository/Models/AppScope/MaskinPortenScopeEntity.cs
new file mode 100644
index 00000000000..879483796b8
--- /dev/null
+++ b/backend/src/Designer/Repository/Models/AppScope/MaskinPortenScopeEntity.cs
@@ -0,0 +1,7 @@
+namespace Altinn.Studio.Designer.Repository.Models.AppScope;
+
+public record MaskinPortenScopeEntity
+{
+ public string Scope { get; set; }
+ public string Description { get; set; }
+}
diff --git a/backend/src/Designer/Repository/ORMImplementation/AppScopesRepository.cs b/backend/src/Designer/Repository/ORMImplementation/AppScopesRepository.cs
new file mode 100644
index 00000000000..3320834b210
--- /dev/null
+++ b/backend/src/Designer/Repository/ORMImplementation/AppScopesRepository.cs
@@ -0,0 +1,50 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Altinn.Studio.Designer.Models;
+using Altinn.Studio.Designer.Repository.Models.AppScope;
+using Altinn.Studio.Designer.Repository.ORMImplementation.Data;
+using Altinn.Studio.Designer.Repository.ORMImplementation.Mappers;
+using Altinn.Studio.Designer.Repository.ORMImplementation.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace Altinn.Studio.Designer.Repository.ORMImplementation;
+
+public class AppScopesRepository : IAppScopesRepository
+{
+ private readonly DesignerdbContext _dbContext;
+
+ public AppScopesRepository(DesignerdbContext dbContext)
+ {
+ _dbContext = dbContext;
+ }
+
+ public async Task GetAppScopesAsync(AltinnRepoContext repoContext, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var appScope = await _dbContext.AppScopes.AsNoTracking().SingleOrDefaultAsync(a => a.Org == repoContext.Org && a.App == repoContext.Repo, cancellationToken);
+
+ return appScope is null ? null : AppScopesMapper.MapToModel(appScope);
+ }
+
+ public async Task UpsertAppScopesAsync(AppScopesEntity appScopesEntity,
+ CancellationToken cancellationToken = default)
+ {
+ AppScopesDbObject existing = await _dbContext.AppScopes.AsNoTracking().SingleOrDefaultAsync(a => a.Org == appScopesEntity.Org && a.App == appScopesEntity.App, cancellationToken);
+
+ var dbObject = existing is null
+ ? AppScopesMapper.MapToDbModel(appScopesEntity)
+ : AppScopesMapper.MapToDbModel(appScopesEntity, existing.Id);
+
+ if (existing is null)
+ {
+ _dbContext.AppScopes.Add(dbObject);
+ }
+ else
+ {
+ _dbContext.Entry(dbObject).State = EntityState.Modified;
+ }
+
+ await _dbContext.SaveChangesAsync(cancellationToken);
+ return AppScopesMapper.MapToModel(dbObject);
+ }
+}
diff --git a/backend/src/Designer/Repository/ORMImplementation/Data/DesignerdbContext.cs b/backend/src/Designer/Repository/ORMImplementation/Data/DesignerdbContext.cs
index 82380acdde9..7131d1b85fb 100644
--- a/backend/src/Designer/Repository/ORMImplementation/Data/DesignerdbContext.cs
+++ b/backend/src/Designer/Repository/ORMImplementation/Data/DesignerdbContext.cs
@@ -12,15 +12,15 @@ public DesignerdbContext(DbContextOptions options)
}
public virtual DbSet Deployments { get; set; }
-
public virtual DbSet Releases { get; set; }
+ public virtual DbSet AppScopes { get; set; }
+
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.UseSerialColumns();
modelBuilder.ApplyConfiguration(new DeploymentConfiguration());
modelBuilder.ApplyConfiguration(new ReleaseConfiguration());
+ modelBuilder.ApplyConfiguration(new AppScopesConfiguration());
base.OnModelCreating(modelBuilder);
}
-
-
}
diff --git a/backend/src/Designer/Repository/ORMImplementation/Data/EntityConfigurations/AppScopesConfiguration.cs b/backend/src/Designer/Repository/ORMImplementation/Data/EntityConfigurations/AppScopesConfiguration.cs
new file mode 100644
index 00000000000..a07757d8589
--- /dev/null
+++ b/backend/src/Designer/Repository/ORMImplementation/Data/EntityConfigurations/AppScopesConfiguration.cs
@@ -0,0 +1,54 @@
+using Altinn.Studio.Designer.Repository.ORMImplementation.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Altinn.Studio.Designer.Repository.ORMImplementation.Data.EntityConfigurations;
+
+public class AppScopesConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("app_scopes", "designer");
+
+ builder.HasKey(e => e.Id).HasName("app_scopes_pkey");
+
+ builder.Property(e => e.Id)
+ .HasColumnName("id")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .UseIdentityColumn();
+
+ builder.Property(e => e.App)
+ .HasColumnType("character varying")
+ .HasColumnName("app")
+ .IsRequired();
+
+ builder.Property(e => e.Org)
+ .HasColumnType("character varying")
+ .HasColumnName("org")
+ .IsRequired();
+
+ builder.Property(e => e.Created)
+ .HasColumnType("timestamptz")
+ .HasColumnName("created");
+
+ builder.Property(e => e.Scopes)
+ .HasColumnType("jsonb")
+ .HasColumnName("scopes")
+ .IsRequired();
+
+ builder.Property(e => e.CreatedBy)
+ .HasColumnType("character varying")
+ .HasColumnName("created_by");
+
+ builder.Property(e => e.LastModifiedBy)
+ .HasColumnType("character varying")
+ .HasColumnName("last_modified_by");
+
+ builder.Property(e => e.Version)
+ .IsRowVersion();
+
+ builder.HasIndex(e => new { e.Org, e.App }, "idx_app_scopes_org_app")
+ .IsUnique();
+ }
+}
diff --git a/backend/src/Designer/Repository/ORMImplementation/Mappers/AppScopesMapper.cs b/backend/src/Designer/Repository/ORMImplementation/Mappers/AppScopesMapper.cs
new file mode 100644
index 00000000000..ca348b6c87d
--- /dev/null
+++ b/backend/src/Designer/Repository/ORMImplementation/Mappers/AppScopesMapper.cs
@@ -0,0 +1,50 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using Altinn.Studio.Designer.Repository.Models.AppScope;
+using Altinn.Studio.Designer.Repository.ORMImplementation.Models;
+
+namespace Altinn.Studio.Designer.Repository.ORMImplementation.Mappers;
+
+public class AppScopesMapper
+{
+ private static readonly JsonSerializerOptions s_jsonOptions = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = false
+ };
+
+ public static AppScopesDbObject MapToDbModel(AppScopesEntity appScopes)
+ {
+ return new AppScopesDbObject
+ {
+ App = appScopes.App,
+ Org = appScopes.Org,
+ Created = appScopes.Created,
+ Scopes = JsonSerializer.Serialize(appScopes.Scopes, s_jsonOptions),
+ CreatedBy = appScopes.CreatedBy,
+ LastModifiedBy = appScopes.LastModifiedBy,
+ Version = appScopes.Version
+ };
+ }
+
+ public static AppScopesDbObject MapToDbModel(AppScopesEntity appScopes, long id)
+ {
+ var dbModel = MapToDbModel(appScopes);
+ dbModel.Id = id;
+ return dbModel;
+ }
+
+ public static AppScopesEntity MapToModel(AppScopesDbObject appScopesDbObject)
+ {
+ return new AppScopesEntity
+ {
+ App = appScopesDbObject.App,
+ Org = appScopesDbObject.Org,
+ Created = appScopesDbObject.Created,
+ Scopes = JsonSerializer.Deserialize>(appScopesDbObject.Scopes, s_jsonOptions),
+ CreatedBy = appScopesDbObject.CreatedBy,
+ LastModifiedBy = appScopesDbObject.LastModifiedBy,
+ Version = appScopesDbObject.Version
+ };
+ }
+}
diff --git a/backend/src/Designer/Repository/ORMImplementation/Models/AppScopesDbObject.cs b/backend/src/Designer/Repository/ORMImplementation/Models/AppScopesDbObject.cs
new file mode 100644
index 00000000000..e6de599b825
--- /dev/null
+++ b/backend/src/Designer/Repository/ORMImplementation/Models/AppScopesDbObject.cs
@@ -0,0 +1,46 @@
+using System;
+
+namespace Altinn.Studio.Designer.Repository.ORMImplementation.Models;
+
+public class AppScopesDbObject
+{
+ ///
+ /// The unique identifier for the object
+ ///
+ public long Id { get; set; }
+
+ ///
+ /// The app name
+ ///
+ public string App { get; set; }
+
+ ///
+ /// The organization name
+ ///
+ public string Org { get; set; }
+
+ ///
+ /// The time the object was created
+ ///
+ public DateTimeOffset Created { get; set; }
+
+ ///
+ /// Maskinporten scopes saved as JSON array
+ ///
+ public string Scopes { get; set; }
+
+ ///
+ /// Identifies the user who created the object
+ ///
+ public string CreatedBy { get; set; }
+
+ ///
+ /// Identifies the user who last modified the object
+ ///
+ public string LastModifiedBy { get; set; }
+
+ ///
+ /// This will be used as concurrency token to handle optimistic concurrency
+ ///
+ public uint Version { get; set; }
+}
diff --git a/backend/src/Designer/Services/Implementation/AppScopesService.cs b/backend/src/Designer/Services/Implementation/AppScopesService.cs
new file mode 100644
index 00000000000..8e06cac12f1
--- /dev/null
+++ b/backend/src/Designer/Services/Implementation/AppScopesService.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Altinn.Studio.Designer.Models;
+using Altinn.Studio.Designer.Repository;
+using Altinn.Studio.Designer.Repository.Models.AppScope;
+using Altinn.Studio.Designer.Services.Interfaces;
+
+namespace Altinn.Studio.Designer.Services.Implementation;
+
+public class AppScopesService : IAppScopesService
+{
+ private readonly IAppScopesRepository _appRepository;
+ private readonly TimeProvider _timeProvider;
+
+ public AppScopesService(IAppScopesRepository appRepository, TimeProvider timeProvider)
+ {
+ _appRepository = appRepository;
+ _timeProvider = timeProvider;
+ }
+
+ public Task GetAppScopesAsync(AltinnRepoContext context,
+ CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return _appRepository.GetAppScopesAsync(context, cancellationToken);
+ }
+
+ public async Task UpsertScopesAsync(AltinnRepoEditingContext editingContext,
+ ISet scopes,
+ CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var appScopes = await _appRepository.GetAppScopesAsync(editingContext, cancellationToken) ??
+ GenerateNewAppScopesEntity(editingContext);
+
+ appScopes.Scopes = scopes;
+ appScopes.LastModifiedBy = editingContext.Developer;
+ return await _appRepository.UpsertAppScopesAsync(appScopes, cancellationToken);
+ }
+
+ private AppScopesEntity GenerateNewAppScopesEntity(AltinnRepoEditingContext context)
+ {
+ return new AppScopesEntity
+ {
+ Org = context.Org,
+ App = context.Repo,
+ CreatedBy = context.Developer,
+ Created = _timeProvider.GetUtcNow(),
+ LastModifiedBy = context.Developer,
+ Scopes = new HashSet()
+ };
+ }
+}
diff --git a/backend/src/Designer/Services/Interfaces/IAppScopesService.cs b/backend/src/Designer/Services/Interfaces/IAppScopesService.cs
new file mode 100644
index 00000000000..f2c7d082af1
--- /dev/null
+++ b/backend/src/Designer/Services/Interfaces/IAppScopesService.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Altinn.Studio.Designer.Models;
+using Altinn.Studio.Designer.Repository.Models.AppScope;
+
+namespace Altinn.Studio.Designer.Services.Interfaces;
+
+public interface IAppScopesService
+{
+ Task GetAppScopesAsync(AltinnRepoContext context, CancellationToken cancellationToken = default);
+ Task UpsertScopesAsync(AltinnRepoEditingContext editingContext, ISet scopes, CancellationToken cancellationToken = default);
+}
diff --git a/backend/src/Designer/appsettings.json b/backend/src/Designer/appsettings.json
index 221a3cf2062..cf846cd73cd 100644
--- a/backend/src/Designer/appsettings.json
+++ b/backend/src/Designer/appsettings.json
@@ -121,8 +121,8 @@
"CookieExpiryTimeInMinutes" : 1,
"AuthorizationDetails": [
{
- "Type": "ansattporten:altinn:service",
- "Resource":"urn:altinn:resource:2480:40"
+ "Type": "ansattporten:altinn:service",
+ "Resource":"urn:altinn:resource:2480:40"
},
{
"Type": "ansattporten:altinn:service",
diff --git a/backend/tests/Designer.Tests/Controllers/ApiTests/ApiTestsBase.cs b/backend/tests/Designer.Tests/Controllers/ApiTests/ApiTestsBase.cs
index c2391e6c936..109345e8f49 100644
--- a/backend/tests/Designer.Tests/Controllers/ApiTests/ApiTestsBase.cs
+++ b/backend/tests/Designer.Tests/Controllers/ApiTests/ApiTestsBase.cs
@@ -1,6 +1,8 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
+using System.Linq;
using System.Net.Http;
using System.Text;
using Designer.Tests.Fixtures;
@@ -62,6 +64,7 @@ protected ApiTestsBase(WebApplicationFactory factory)
{
Factory = factory;
SetupDirtyHackIfLinux();
+ InitializeJsonConfigOverrides();
}
///
@@ -74,7 +77,7 @@ protected virtual HttpClient GetTestClient()
string configPath = GetConfigPath();
IConfiguration configuration = new ConfigurationBuilder()
.AddJsonFile(configPath, false, false)
- .AddJsonStream(GenerateOidcConfigJsonConfig())
+ .AddJsonStream(GenerateJsonOverrideConfig())
.AddEnvironmentVariables()
.Build();
@@ -84,7 +87,7 @@ protected virtual HttpClient GetTestClient()
builder.ConfigureAppConfiguration((_, conf) =>
{
conf.AddJsonFile(configPath);
- conf.AddJsonStream(GenerateOidcConfigJsonConfig());
+ conf.AddJsonStream(GenerateJsonOverrideConfig());
});
builder.ConfigureTestServices(ConfigureTestServices);
builder.ConfigureTestServices(services =>
@@ -115,9 +118,13 @@ protected virtual void Dispose(bool disposing)
{
}
- protected Stream GenerateOidcConfigJsonConfig()
+ protected List JsonConfigOverrides;
+
+ private void InitializeJsonConfigOverrides()
{
- string configOverride = $@"
+ JsonConfigOverrides =
+ [
+ $@"
{{
""OidcLoginSettings"": {{
""ClientId"": ""{Guid.NewGuid()}"",
@@ -140,8 +147,26 @@ protected Stream GenerateOidcConfigJsonConfig()
""CookieExpiryTimeInMinutes"" : 59
}}
}}
- ";
- var configStream = new MemoryStream(Encoding.UTF8.GetBytes(configOverride));
+ "
+ ];
+ }
+
+
+ private Stream GenerateJsonOverrideConfig()
+ {
+ var overrideJson = Newtonsoft.Json.Linq.JObject.Parse(JsonConfigOverrides.First());
+ if (JsonConfigOverrides.Count > 1)
+ {
+ foreach (string jsonConfig in JsonConfigOverrides)
+ {
+ overrideJson.Merge(Newtonsoft.Json.Linq.JObject.Parse(jsonConfig), new Newtonsoft.Json.Linq.JsonMergeSettings
+ {
+ MergeArrayHandling = Newtonsoft.Json.Linq.MergeArrayHandling.Union
+ });
+ }
+ }
+ string overrideJsonString = overrideJson.ToString();
+ var configStream = new MemoryStream(Encoding.UTF8.GetBytes(overrideJsonString));
configStream.Seek(0, SeekOrigin.Begin);
return configStream;
}
diff --git a/backend/tests/Designer.Tests/Controllers/ApiTests/DbDesignerEndpointsTestsBase.cs b/backend/tests/Designer.Tests/Controllers/ApiTests/DbDesignerEndpointsTestsBase.cs
new file mode 100644
index 00000000000..6f52863246c
--- /dev/null
+++ b/backend/tests/Designer.Tests/Controllers/ApiTests/DbDesignerEndpointsTestsBase.cs
@@ -0,0 +1,30 @@
+using System;
+using Designer.Tests.Fixtures;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Xunit;
+
+namespace Designer.Tests.Controllers.ApiTests
+{
+ [Trait("Category", "DbIntegrationTest")]
+ [Collection(nameof(DesignerDbCollection))]
+ public abstract class DbDesignerEndpointsTestsBase : DesignerEndpointsTestsBase
+ where TControllerTest : class
+ {
+ protected readonly DesignerDbFixture DesignerDbFixture;
+
+ protected DbDesignerEndpointsTestsBase(WebApplicationFactory factory,
+ DesignerDbFixture designerDbFixture) : base(factory)
+ {
+ DesignerDbFixture = designerDbFixture;
+ JsonConfigOverrides.Add($@"
+ {{
+ ""PostgreSQLSettings"": {{
+ ""ConnectionString"": ""{DesignerDbFixture.ConnectionString}"",
+ ""DesignerDbPwd"": """"
+ }}
+ }}
+ ");
+ }
+
+ }
+}
diff --git a/backend/tests/Designer.Tests/Controllers/ApiTests/TestSchemeProvider.cs b/backend/tests/Designer.Tests/Controllers/ApiTests/TestSchemeProvider.cs
index a2701804cd6..f2e054f65ec 100644
--- a/backend/tests/Designer.Tests/Controllers/ApiTests/TestSchemeProvider.cs
+++ b/backend/tests/Designer.Tests/Controllers/ApiTests/TestSchemeProvider.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
+using Altinn.Studio.Designer;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Options;
@@ -21,7 +22,7 @@ protected TestSchemeProvider(IOptions options, IDictionar
public override Task GetSchemeAsync(string name)
{
// Replace cookies scheme used in oidc setup with test scheme
- if (name == CookieAuthenticationDefaults.AuthenticationScheme)
+ if (name is CookieAuthenticationDefaults.AuthenticationScheme or AnsattPortenConstants.AnsattportenAuthenticationScheme)
{
return base.GetSchemeAsync(TestAuthConstants.TestAuthenticationScheme);
}
diff --git a/backend/tests/Designer.Tests/Controllers/AppScopesController/Base/AppScopesControllerTestsBase.cs b/backend/tests/Designer.Tests/Controllers/AppScopesController/Base/AppScopesControllerTestsBase.cs
new file mode 100644
index 00000000000..4e6e468ae06
--- /dev/null
+++ b/backend/tests/Designer.Tests/Controllers/AppScopesController/Base/AppScopesControllerTestsBase.cs
@@ -0,0 +1,24 @@
+using Designer.Tests.Controllers.ApiTests;
+using Designer.Tests.Fixtures;
+using Microsoft.AspNetCore.Mvc.Testing;
+
+namespace Designer.Tests.Controllers.AppScopesController.Base;
+
+public class AppScopesControllerTestsBase : DbDesignerEndpointsTestsBase
+where TControllerTest : class
+{
+ public AppScopesControllerTestsBase(WebApplicationFactory factory, DesignerDbFixture designerDbFixture) : base(factory, designerDbFixture)
+ {
+ JsonConfigOverrides.Add($@"
+ {{
+ ""FeatureManagement"": {{
+ ""AnsattPorten"": true
+ }},
+ ""AnsattPortenLoginSettings"": {{
+ ""ClientId"": ""non-empty-for-testing"",
+ ""ClientSecret"": ""non-empty-for-testing""
+ }}
+ }}
+ ");
+ }
+}
diff --git a/backend/tests/Designer.Tests/Controllers/AppScopesController/GetAppScopesTests.cs b/backend/tests/Designer.Tests/Controllers/AppScopesController/GetAppScopesTests.cs
new file mode 100644
index 00000000000..04c9764b761
--- /dev/null
+++ b/backend/tests/Designer.Tests/Controllers/AppScopesController/GetAppScopesTests.cs
@@ -0,0 +1,58 @@
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Altinn.Studio.Designer.Models.Dto;
+using Designer.Tests.Controllers.AppScopesController.Base;
+using Designer.Tests.DbIntegrationTests;
+using Designer.Tests.Fixtures;
+using FluentAssertions;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Xunit;
+
+namespace Designer.Tests.Controllers.AppScopesController;
+
+public class GetAppScopesTests : AppScopesControllerTestsBase, IClassFixture>
+{
+ private static string VersionPrefix(string org, string repository) =>
+ $"/designer/api/{org}/{repository}/app-scopes";
+
+ public GetAppScopesTests(WebApplicationFactory factory, DesignerDbFixture designerDbFixture) : base(factory, designerDbFixture)
+ {
+ }
+
+ [Theory]
+ [InlineData("ttd", "non-existing-app")]
+ public async Task GetAppScopes_Should_ReturnOk_WithEmptyScopes_IfRecordDoesntExists(string org, string app)
+ {
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get
+ , VersionPrefix(org, app));
+
+ using var response = await HttpClient.SendAsync(httpRequestMessage);
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ AppScopesResponse repsponseContent = await response.Content.ReadAsAsync();
+ repsponseContent.Scopes.Should().BeEmpty();
+ }
+
+ [Theory]
+ [InlineData("ttd", "empty-app")]
+ public async Task GetAppScopes_Should_ReturnOk_WithScopes_IfRecordExists(string org, string app)
+ {
+ var entity = EntityGenerationUtils.AppScopes.GenerateAppScopesEntity(org, app, 4);
+ await DesignerDbFixture.PrepareAppScopesEntityInDatabaseAsync(entity);
+
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get
+ , VersionPrefix(org, app));
+
+ using var response = await HttpClient.SendAsync(httpRequestMessage);
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ AppScopesResponse responseContent = await response.Content.ReadAsAsync();
+ responseContent.Scopes.Should().HaveCount(4);
+
+ foreach (MaskinPortenScopeDto scope in responseContent.Scopes)
+ {
+ entity.Scopes.Should().Contain(x => scope.Scope == x.Scope && scope.Description == x.Description);
+ }
+ }
+}
diff --git a/backend/tests/Designer.Tests/Controllers/AppScopesController/GetScopesFromMaskinPortenTests.cs b/backend/tests/Designer.Tests/Controllers/AppScopesController/GetScopesFromMaskinPortenTests.cs
new file mode 100644
index 00000000000..1068af40222
--- /dev/null
+++ b/backend/tests/Designer.Tests/Controllers/AppScopesController/GetScopesFromMaskinPortenTests.cs
@@ -0,0 +1,89 @@
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Text.Json.Nodes;
+using System.Threading.Tasks;
+using Altinn.Studio.Designer.Models.Dto;
+using Designer.Tests.Controllers.AppScopesController.Base;
+using Designer.Tests.Controllers.AppScopesController.Utils;
+using Designer.Tests.Fixtures;
+using FluentAssertions;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Xunit;
+
+namespace Designer.Tests.Controllers.AppScopesController;
+
+public class GetScopesFromMaskinPortenTests : AppScopesControllerTestsBase, IClassFixture>, IClassFixture
+{
+ private static string VersionPrefix(string org, string repository) =>
+ $"/designer/api/{org}/{repository}/app-scopes/maskinporten";
+
+ private readonly MockServerFixture _mockServerFixture;
+
+ public GetScopesFromMaskinPortenTests(WebApplicationFactory factory, DesignerDbFixture designerDbFixture, MockServerFixture mockServerFixture) : base(factory, designerDbFixture)
+ {
+ _mockServerFixture = mockServerFixture;
+ JsonConfigOverrides.Add(
+ $$"""
+ {
+ "MaskinPortenHttpClientSettings" : {
+ "BaseUrl": "{{mockServerFixture.MockApi.Url}}"
+ }
+ }
+ """
+ );
+ }
+
+ [Theory]
+ [MemberData(nameof(TestData))]
+ public async Task GetScopesFromMaskinPortens_Should_ReturnOk(string org, string app, string maskinPortenResponse)
+ {
+ _mockServerFixture.PrepareMaskinPortenScopesResponse(maskinPortenResponse);
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get
+ , VersionPrefix(org, app));
+
+ using var response = await HttpClient.SendAsync(httpRequestMessage);
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ AppScopesResponse repsponseContent = await response.Content.ReadAsAsync();
+ JsonArray array = (JsonArray)JsonNode.Parse(maskinPortenResponse);
+ repsponseContent.Scopes.Count.Should().Be(array.Count);
+ }
+
+
+ public static IEnumerable