Skip to content

Commit

Permalink
Merge branch 'main' into subform-table-manually-editing-column-name
Browse files Browse the repository at this point in the history
  • Loading branch information
lassopicasso committed Jan 14, 2025
2 parents a5119e8 + 6b86ab2 commit 403ce43
Show file tree
Hide file tree
Showing 16 changed files with 315 additions and 29 deletions.
17 changes: 16 additions & 1 deletion backend/src/Designer/Controllers/UserController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Helpers;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Models.Dto;
using Altinn.Studio.Designer.Services.Interfaces;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Authorization;
Expand All @@ -17,16 +20,18 @@ public class UserController : ControllerBase
{
private readonly IGitea _giteaApi;
private readonly IAntiforgery _antiforgery;
private readonly IUserService _userService;

/// <summary>
/// Initializes a new instance of the <see cref="UserController"/> class.
/// </summary>
/// <param name="giteaWrapper">the gitea wrapper</param>
/// <param name="antiforgery">Access to the antiforgery system in .NET Core</param>
public UserController(IGitea giteaWrapper, IAntiforgery antiforgery)
public UserController(IGitea giteaWrapper, IAntiforgery antiforgery, IUserService userService)
{
_giteaApi = giteaWrapper;
_antiforgery = antiforgery;
_userService = userService;
}

/// <summary>
Expand Down Expand Up @@ -80,6 +85,16 @@ public async Task<IActionResult> PutStarred(string org, string repository)
return success ? NoContent() : StatusCode(418);
}

[HttpGet]
[Route("org-permissions/{org}")]
public async Task<IActionResult> HasAccessToCreateRepository(string org)
{
string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext);
AltinnOrgContext editingContext = AltinnOrgContext.FromOrg(org, developer);
UserOrgPermission userOrg = await _userService.GetUserOrgPermission(editingContext);
return Ok(userOrg);
}

/// <summary>
/// Removes the star marking on the specified repository.
/// </summary>
Expand Down
9 changes: 9 additions & 0 deletions backend/src/Designer/Helpers/Guard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ public static void AssertArgumentNotNullOrWhiteSpace(string value, string argume
}
}

public static void AssertValidateOrganization(string org)
{
AssertNotNullOrEmpty(org, nameof(org));
if (!AltinnRegexes.AltinnOrganizationNameRegex().IsMatch(org))
{
throw new ArgumentException("Provided organization name is not valid");
}
}

/// <summary>
/// Asserts value not null.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions backend/src/Designer/Infrastructure/ServiceRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public static IServiceCollection RegisterServiceImplementations(this IServiceCol
services.AddScoped<IReleaseRepository, ReleaseRepository>();
services.AddScoped<IDeploymentRepository, DeploymentRepository>();
services.AddScoped<IAppScopesRepository, AppScopesRepository>();
services.AddScoped<IUserService, UserService>();
services.AddTransient<IReleaseService, ReleaseService>();
services.AddTransient<IDeploymentService, DeploymentService>();
services.AddTransient<IAppScopesService, AppScopesService>();
Expand Down
23 changes: 23 additions & 0 deletions backend/src/Designer/Models/AltinnOrgContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Altinn.Studio.Designer.Helpers;

namespace Altinn.Studio.Designer.Models;

public class AltinnOrgContext
{
public string Org { get; }
public string DeveloperName { get; }

private AltinnOrgContext(string org, string developerName)
{
Guard.AssertValidateOrganization(org);
Org = org;

Guard.AssertArgumentNotNullOrWhiteSpace(developerName, nameof(developerName));
DeveloperName = developerName;
}

public static AltinnOrgContext FromOrg(string org, string developerName)
{
return new AltinnOrgContext(org, developerName);
}
}
14 changes: 2 additions & 12 deletions backend/src/Designer/Models/AltinnRepoContext.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using Altinn.Studio.Designer.Helpers;
using Altinn.Studio.Designer.Helpers;

namespace Altinn.Studio.Designer.Models
{
Expand All @@ -21,21 +20,12 @@ public class AltinnRepoContext

protected AltinnRepoContext(string org, string repo)
{
ValidateOrganization(org);
Guard.AssertValidateOrganization(org);
Guard.AssertValidAppRepoName(repo);
Org = org;
Repo = repo;
}

private void ValidateOrganization(string org)
{
Guard.AssertNotNullOrEmpty(org, nameof(org));
if (!AltinnRegexes.AltinnOrganizationNameRegex().IsMatch(org))
{
throw new ArgumentException("Provided organization name is not valid");
}
}

public static AltinnRepoContext FromOrgRepo(string org, string repo)
{
return new AltinnRepoContext(org, repo);
Expand Down
6 changes: 6 additions & 0 deletions backend/src/Designer/Models/Dto/UserOrgPermission.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Altinn.Studio.Designer.Models.Dto;

public class UserOrgPermission
{
public bool CanCreateOrgRepo { get; set; }
}
2 changes: 2 additions & 0 deletions backend/src/Designer/RepositoryClient/Model/Team.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public class Team
/// </summary>
public string Name { get; set; }

public bool CanCreateOrgRepo { get; set; }

/// <summary>
/// The organization that owns the team
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,17 @@ public async Task<List<Team>> GetTeams()
HttpResponseMessage response = await _httpClient.GetAsync(url);
if (response.StatusCode == HttpStatusCode.OK)
{
teams = await response.Content.ReadAsAsync<List<Team>>() ?? new List<Team>();
string jsonString = await response.Content.ReadAsStringAsync();
var deserializeOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
};

teams = JsonSerializer.Deserialize<List<Team>>(jsonString, deserializeOptions) ?? new List<Team>();
}
else
{
_logger.LogError("Cold not retrieve teams for user " + AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext) + " GetTeams failed with statuscode " + response.StatusCode);
_logger.LogError("Could not retrieve teams for user " + AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext) + " GetTeams failed with status code " + response.StatusCode);
}

return teams;
Expand Down
42 changes: 42 additions & 0 deletions backend/src/Designer/Services/Implementation/UserService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Models.Dto;
using Altinn.Studio.Designer.RepositoryClient.Model;
using Altinn.Studio.Designer.Services.Interfaces;

namespace Altinn.Studio.Designer.Services.Implementation;

public class UserService : IUserService
{
private readonly IGitea _giteaApi;

public UserService(IGitea giteaApi)
{
_giteaApi = giteaApi;
}

public async Task<UserOrgPermission> GetUserOrgPermission(AltinnOrgContext altinnOrgContext)
{
bool canCreateOrgRepo = await HasPermissionToCreateOrgRepo(altinnOrgContext);
return new UserOrgPermission() { CanCreateOrgRepo = canCreateOrgRepo };
}

private bool IsUserSelfOrg(string developerName, string org)
{
return developerName == org;
}

private async Task<bool> HasPermissionToCreateOrgRepo(AltinnOrgContext altinnOrgContext)
{
List<Team> teams = await _giteaApi.GetTeams();
return IsUserSelfOrg(altinnOrgContext.DeveloperName, altinnOrgContext.Org) ||
teams.Any(team => CheckPermissionToCreateOrgRepo(team, altinnOrgContext.Org));
}

private static bool CheckPermissionToCreateOrgRepo(Team team, string org)
{
return team.CanCreateOrgRepo && team.Organization.Username == org;
}
}
10 changes: 10 additions & 0 deletions backend/src/Designer/Services/Interfaces/IUserService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Altinn.Studio.Designer.Models;
using Altinn.Studio.Designer.Models.Dto;

namespace Altinn.Studio.Designer.Services.Interfaces;

public interface IUserService
{
public Task<UserOrgPermission> GetUserOrgPermission(AltinnOrgContext altinnOrgContext);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Altinn.Studio.Designer.TypedHttpClients.DelegatingHandlers;

public class AzureDevOpsTokenDelegatingHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);

if (response.StatusCode == HttpStatusCode.Unauthorized)
{
return new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent("Failed to interact with Azure DevOps. Contact system support.")
};
}

return response;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public static class TypedHttpClientRegistration
public static IServiceCollection RegisterTypedHttpClients(this IServiceCollection services, IConfiguration config)
{
services.AddHttpClient();
services.AddTransient<AzureDevOpsTokenDelegatingHandler>();
services.AddTransient<EnsureSuccessHandler>();
services.AddTransient<PlatformBearerTokenHandler>();
services.AddAzureDevOpsTypedHttpClient(config);
Expand Down Expand Up @@ -70,7 +71,7 @@ private static IHttpClientBuilder AddAzureDevOpsTypedHttpClient(this IServiceCol
client.BaseAddress = new Uri($"{azureDevOpsSettings.BaseUri}build/builds/");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", token);
}).AddHttpMessageHandler<EnsureSuccessHandler>();
}).AddHttpMessageHandler<AzureDevOpsTokenDelegatingHandler>().AddHttpMessageHandler<EnsureSuccessHandler>();
}

private static IHttpClientBuilder AddKubernetesWrapperTypedHttpClient(this IServiceCollection services)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Altinn.Studio.Designer.Models.Dto;
using Altinn.Studio.Designer.RepositoryClient.Model;
using Designer.Tests.Fixtures;
using Designer.Tests.Utils;
Expand All @@ -14,8 +15,9 @@ namespace Designer.Tests.GiteaIntegrationTests
{
public class UserControllerGiteaIntegrationTests : GiteaIntegrationTestsBase<UserControllerGiteaIntegrationTests>
{

public UserControllerGiteaIntegrationTests(GiteaWebAppApplicationFactoryFixture<Program> factory, GiteaFixture giteaFixture, SharedDesignerHttpClientProvider sharedDesignerHttpClientProvider) : base(factory, giteaFixture, sharedDesignerHttpClientProvider)
public UserControllerGiteaIntegrationTests(GiteaWebAppApplicationFactoryFixture<Program> factory,
GiteaFixture giteaFixture, SharedDesignerHttpClientProvider sharedDesignerHttpClientProvider) : base(
factory, giteaFixture, sharedDesignerHttpClientProvider)
{
}

Expand All @@ -31,10 +33,8 @@ public async Task GetCurrentUser_ShouldReturnOk(string expectedUserName, string
response.StatusCode.Should().Be(HttpStatusCode.OK);
response.Headers.First(h => h.Key == "Set-Cookie").Value.Should().Contain(e => e.Contains("XSRF-TOKEN"));
string content = await response.Content.ReadAsStringAsync();
var user = JsonSerializer.Deserialize<User>(content, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
var user = JsonSerializer.Deserialize<User>(content,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

user.Login.Should().Be(expectedUserName);
user.Email.Should().Be(expectedEmail);
Expand Down Expand Up @@ -62,16 +62,40 @@ public async Task StarredEndpoints_ShouldBehaveAsExpected(string org)
string targetRepo = TestDataHelper.GenerateTestRepoName();
await CreateAppUsingDesigner(org, targetRepo);

using var putStarredResponse = await HttpClient.PutAsync($"designer/api/user/starred/{org}/{targetRepo}", null);
using var putStarredResponse =
await HttpClient.PutAsync($"designer/api/user/starred/{org}/{targetRepo}", null);
putStarredResponse.StatusCode.Should().Be(HttpStatusCode.NoContent);
await GetAndVerifyStarredRepos(targetRepo);

using var deleteStarredResponse = await HttpClient.DeleteAsync($"designer/api/user/starred/{org}/{targetRepo}");
using var deleteStarredResponse =
await HttpClient.DeleteAsync($"designer/api/user/starred/{org}/{targetRepo}");
deleteStarredResponse.StatusCode.Should().Be(HttpStatusCode.NoContent);

await GetAndVerifyStarredRepos();
}

[Theory]
[InlineData(GiteaConstants.TestOrgUsername, true)]
[InlineData("OtherOrg", false)]
public async Task HasAccessToCreateRepository_ShouldReturnCorrectPermissions(string org, bool expectedCanCreate)
{
string requestUrl = $"designer/api/user/org-permissions/{org}";

using var response = await HttpClient.GetAsync(requestUrl);

response.StatusCode.Should().Be(HttpStatusCode.OK);
string content = await response.Content.ReadAsStringAsync();
var deserializeOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

var userOrgPermission = JsonSerializer.Deserialize<Team>(content, deserializeOptions);

userOrgPermission.Should().NotBeNull();
userOrgPermission.CanCreateOrgRepo.Should().Be(expectedCanCreate);
}

private async Task GetAndVerifyStarredRepos(params string[] expectedStarredRepos)
{
using var response = await HttpClient.GetAsync("designer/api/user/starred");
Expand All @@ -83,6 +107,5 @@ private async Task GetAndVerifyStarredRepos(params string[] expectedStarredRepos
content.Should().Contain(r => r.Name == expectedStarredRepo);
}
}

}
}
26 changes: 22 additions & 4 deletions backend/tests/Designer.Tests/Helpers/GuardTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using System;

using Altinn.Studio.Designer.Helpers;

using Xunit;

namespace Designer.Tests.Helpers
Expand All @@ -12,7 +10,8 @@ public class GuardTests
[InlineData("filename.xsd", ".xs")]
[InlineData("path/to/filename.json", "json")]
[InlineData("path/to/filename.schema.json", "jsonschema")]
public void AssertFileExtensionIsOfType_InCorrectType_ShouldThrowException(string file, string incorrectExtension)
public void AssertFileExtensionIsOfType_InCorrectType_ShouldThrowException(string file,
string incorrectExtension)
{
Assert.Throws<ArgumentException>(() => Guard.AssertFileExtensionIsOfType(file, incorrectExtension));
}
Expand All @@ -21,7 +20,8 @@ public void AssertFileExtensionIsOfType_InCorrectType_ShouldThrowException(strin
[InlineData("filename.xsd", ".xsd")]
[InlineData("path/to/filename.json", ".json")]
[InlineData("path/to/filename.schema.json", ".json")]
public void AssertFileExtensionIsOfType_CorrectType_ShouldNotThrowException(string file, string correctExtension)
public void AssertFileExtensionIsOfType_CorrectType_ShouldNotThrowException(string file,
string correctExtension)
{
Guard.AssertFileExtensionIsOfType(file, correctExtension);
Assert.True(true);
Expand All @@ -43,5 +43,23 @@ public void AssertValidAppRepoName_InvalidName_ShouldThrowException(string name)
{
Assert.Throws<ArgumentException>(() => Guard.AssertValidAppRepoName(name));
}

[Theory]
[InlineData("ValidOrgName")]
public void AssertValidateOrganization_ValidOrg_ShouldNotThrowException(string org)
{
Guard.AssertValidateOrganization(org);
Assert.True(true);
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("Invalid@OrgName")]
public void AssertValidateOrganization_InvalidOrg_ShouldThrowException(string org)
{
Assert.Throws<ArgumentException>(() => Guard.AssertValidateOrganization(org));
}
}
}
Loading

0 comments on commit 403ce43

Please sign in to comment.