Skip to content
Merged
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
Expand Up @@ -27,8 +27,8 @@ public ProjectDetails ToProjectDetails()
CreationDate,
SavedFiles.ToDictionary(x => x.Key, x => x.Value.Select(v => v.ToFileData()).ToList()),
LoadedFiles.ToDictionary(x => x.Key, x => x.Value.Select(v => v.ToFileData()).ToList()),
Linkers.ToList(),
InstalledPlugins.Select(x => x.ToPluginInstallationConfig()).ToList()
[.. Linkers],
[.. InstalledPlugins.Select(x => x.ToPluginInstallationConfig())]
);
}

Expand All @@ -48,9 +48,10 @@ public static MongodbProjectModel From(ProjectDetails projectDetails)
x => x.Value.Select(MongodbFileData.From).ToList()
),
Linkers = projectDetails.Linkers,
InstalledPlugins = projectDetails
.InstalledPlugins.Select(MongodbPluginInstallationConfig.From)
.ToList(),
InstalledPlugins =
[
.. projectDetails.InstalledPlugins.Select(MongodbPluginInstallationConfig.From),
],
};
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using ScriptBee.Persistence.Mongodb.Repository;

namespace ScriptBee.Persistence.Mongodb.Entity;
Expand All @@ -7,10 +8,13 @@ namespace ScriptBee.Persistence.Mongodb.Entity;
public class MongodbResourceMember : IDocument
{
[BsonId]
public required string Id { get; set; }
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; } = null!;

public required string ResourceType { get; init; }
public required string ResourceId { get; init; }
public required string MemberType { get; init; }
public required string MemberId { get; init; }
public required string Role { get; init; }
public required DateTimeOffset AssignedAt { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public IServiceCollection AddResourceMembers(IMongoDatabase mongoDatabase)
{
return services
.AddMongoCollection<MongodbResourceMember>(mongoDatabase, "ResourceMembers")
.AddSingleton<IResourceMemberService, ResourceMembersPersistenceAdapter>();
.AddSingleton<IGetResourceRole, ResourceMembersPersistenceAdapter>()
.AddSingleton<ISetResourceRole, ResourceMembersPersistenceAdapter>();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ namespace ScriptBee.Persistence.Mongodb;

public sealed class ResourceMembersPersistenceAdapter(
IMongoRepository<MongodbResourceMember> mongoRepository
) : IResourceMemberService
) : IGetResourceRole, ISetResourceRole
{
private const string ProjectResourceType = "project";
private const string UserMemberType = "user";
private const string GroupMemberType = "group";

public async Task<UserRole?> GetResourceRole(
public async Task<UserRole?> GetRole(
UserId userId,
List<UserGroup> groups,
OneOf<ProjectId> resourceId,
Expand Down Expand Up @@ -50,4 +50,25 @@ [.. groups.Select(g => g.Value)]

return new UserRole(resourceMember.Role);
}

public async Task SetRoleForUser(
UserId userId,
ProjectId projectId,
UserRole role,
CancellationToken cancellationToken
)
{
var model = new MongodbResourceMember
{
Id = null!,
ResourceType = ProjectResourceType,
ResourceId = projectId.Value,
MemberType = UserMemberType,
MemberId = userId.Value,
Role = role.Value,
AssignedAt = DateTimeOffset.UtcNow,
};

await mongoRepository.CreateDocument(model, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net;
using ScriptBee.Web.Auth.Contracts;

namespace ScriptBee.Web.Auth;

Expand All @@ -7,9 +8,9 @@ public partial class AuthorizeExternally(
ILogger<ExternalAuthorizationActionAuthorizationHandler> logger
) : IAuthorizeExternally
{
private readonly HttpClient _httpClient = httpClientFactory.CreateClient(
"ExternalAuthorizationClient"
);
public const string ClientName = "ExternalAuthorizationClient";

private readonly HttpClient _httpClient = httpClientFactory.CreateClient(ClientName);

public async Task<bool> IsAllowed(
ExternalAuthorizationRequest request,
Expand All @@ -36,7 +37,7 @@ CancellationToken cancellationToken
cancellationToken: cancellationToken
);

return result is { Allow: true };
return result is { Result: true };
}

[LoggerMessage(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;

namespace ScriptBee.Web.Auth.Contracts;

public class DefaultCreatorRoleResponse
{
[JsonPropertyName("result")]
public required string Result { get; init; }
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;

namespace ScriptBee.Web.Auth;
namespace ScriptBee.Web.Auth.Contracts;

public class ExternalAuthorizationRequest
{
Expand Down Expand Up @@ -43,6 +43,6 @@ public class ExternalAuthorizationResource

public class ExternalAuthorizationResponse
{
[JsonPropertyName("allow")]
public required bool Allow { get; init; }
[JsonPropertyName("result")]
public required bool Result { get; init; }
}
61 changes: 61 additions & 0 deletions ScriptBeeWebApp/src/Gateway/Adapters/Web/Auth/CurrentUser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Security.Claims;
using Microsoft.Extensions.Options;
using ScriptBee.Domain.Model.User;
using ScriptBee.Web.Config;

namespace ScriptBee.Web.Auth;

public sealed class CurrentUser(UserId id)
{
public UserId Id => id;

public static ValueTask<CurrentUser?> BindAsync(HttpContext context)
{
var user = context.User;

var authConfigOptions = context.RequestServices.GetRequiredService<
IOptions<AuthenticationConfig>
>();
var authConfig = authConfigOptions.Value;

if (authConfig.IsDevelopment)
{
return ValueTask.FromResult<CurrentUser?>(new CurrentUser(new UserId("")));
}

if (user.Identity?.IsAuthenticated != true)
{
return ValueTask.FromResult<CurrentUser?>(null);
}

var userId = ExtractUserIdFromClaims(user, authConfig);
return ValueTask.FromResult<CurrentUser?>(new CurrentUser(userId));
}

public static UserId ExtractUserIdFromClaims(
ClaimsPrincipal claimsPrincipal,
AuthenticationConfig authConfig
)
{
var userId =
authConfig.UserIdClaim != null
? claimsPrincipal.FindFirst(authConfig.UserIdClaim)?.Value
: claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
userId ??= "";

return new UserId(userId);
}

public static List<UserGroup> ExtractGroupsFromClaims(
ClaimsPrincipal claimsPrincipal,
AuthenticationConfig authConfig
)
{
var groups =
authConfig.GroupsClaim == null
? []
: claimsPrincipal.FindAll(authConfig.GroupsClaim).Select(c => c.Value).ToList();

return [.. groups.Select(g => new UserGroup(g))];
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
using System.Security.Claims;
using Microsoft.Extensions.Options;
using OneOf;
using ScriptBee.Domain.Model.Project;
using ScriptBee.Domain.Model.User;
using ScriptBee.Ports.Permissions;
using ScriptBee.Web.Auth.Contracts;
using ScriptBee.Web.Config;

namespace ScriptBee.Web.Auth;

public sealed class ExternalAuthorizationContextProvider(
IResourceMemberService resourceMemberService,
IGetResourceRole getResourceRole,
IOptions<AuthenticationConfig> authConfigOptions
) : IExternalAuthorizationContextProvider
{
Expand All @@ -20,7 +20,10 @@ CancellationToken cancellationToken
)
{
var routeData = httpContext.GetRouteData();
var (userId, groups) = ExtractFromClaims(httpContext);
var authConfig = authConfigOptions.Value;
var claimsPrincipal = httpContext.User;
var userId = CurrentUser.ExtractUserIdFromClaims(claimsPrincipal, authConfig);
var groups = CurrentUser.ExtractGroupsFromClaims(claimsPrincipal, authConfig);

if (
routeData.Values.TryGetValue("projectId", out var projectIdObj)
Expand All @@ -39,28 +42,6 @@ CancellationToken cancellationToken
return GetGlobalRequest(action, userId, groups);
}

private (UserId userId, List<UserGroup> groups) ExtractFromClaims(HttpContext httpContext)
{
var authConfig = authConfigOptions.Value;
var claimsPrincipal = httpContext.User;

var userId =
authConfig.UserIdClaim != null
? claimsPrincipal.FindFirst(authConfig.UserIdClaim)?.Value
: claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
userId ??= "";

var groups =
authConfig.GroupsClaim == null
? []
: claimsPrincipal.FindAll(authConfig.GroupsClaim).Select(c => c.Value).ToList();

return new ValueTuple<UserId, List<UserGroup>>(
new UserId(userId),
[.. groups.Select(g => new UserGroup(g))]
);
}

private async Task<ExternalAuthorizationRequest> GetProjectRequest(
string action,
UserId userId,
Expand All @@ -69,7 +50,7 @@ private async Task<ExternalAuthorizationRequest> GetProjectRequest(
CancellationToken cancellationToken
)
{
var resourceRole = await resourceMemberService.GetResourceRole(
var resourceRole = await getResourceRole.GetRole(
userId,
groups,
OneOf<ProjectId>.FromT0(projectId),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using ScriptBee.Domain.Model.User;
using ScriptBee.Ports.Permissions;
using ScriptBee.Web.Auth.Contracts;

namespace ScriptBee.Web.Auth;

public sealed class GetDefaultCreatorRole(IHttpClientFactory httpClientFactory)
: IGetDefaultCreatorRole
{
public const string ClientName = "GetDefaultCreatorRole";

private readonly HttpClient _httpClient = httpClientFactory.CreateClient(ClientName);

public async Task<UserRole> GetRole(CancellationToken cancellationToken)
{
var response = await _httpClient.GetFromJsonAsync<DefaultCreatorRoleResponse>(
"",
cancellationToken: cancellationToken
);

return new UserRole(response!.Result);
}
}

public sealed class GetDevAuthCreatorRole : IGetDefaultCreatorRole
{
public Task<UserRole> GetRole(CancellationToken cancellationToken)
{
return Task.FromResult(new UserRole("DevAdmin"));
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using ScriptBee.Web.Auth.Contracts;

namespace ScriptBee.Web.Auth;

public interface IAuthorizeExternally
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using ScriptBee.Web.Auth.Contracts;

namespace ScriptBee.Web.Auth;

public interface IExternalAuthorizationContextProvider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ public class AuthenticationConfig
public string? AuthWellknownEndpointUrl { get; init; }
public string? ClientId { get; init; }
public string? Scope { get; init; }
public string? ExternalAuthorizationUrl { get; init; }
public string? UserIdClaim { get; init; }
public string? GroupsClaim { get; init; }
public string? ExternalAuthorizationUrl { get; init; }
public string? DefaultCreatorRoleUrl { get; init; }

public bool IsDevelopment =>
AuthMode?.Equals("Development", StringComparison.OrdinalIgnoreCase) ?? false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
using System.ComponentModel;
using ScriptBee.Domain.Model.User;
using ScriptBee.UseCases.Gateway;

namespace ScriptBee.Web.EndpointDefinitions.Project.Contracts;

[Description("Command used to create a new project.")]
public record WebCreateProjectCommand(string Id, string Name)
{
public CreateProjectCommand Map()
public CreateProjectCommand Map(UserId userId)
{
return new CreateProjectCommand(Id, Name);
return new CreateProjectCommand(Id, Name, userId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using ScriptBee.Common.Web.Validation;
using ScriptBee.Service.Gateway;
using ScriptBee.UseCases.Gateway;
using ScriptBee.Web.Auth;
using ScriptBee.Web.EndpointDefinitions.Project.Contracts;
using ScriptBee.Web.Exceptions;

Expand All @@ -28,13 +29,14 @@ public void DefineEndpoints(IEndpointRouteBuilder app)
private static async Task<
Results<Created<WebCreateProjectResponse>, Conflict<ProblemDetails>>
> CreateProject(
CurrentUser currentUser,
HttpContext context,
[FromBody] WebCreateProjectCommand command,
ICreateProjectUseCase useCase,
CancellationToken cancellationToken = default
)
{
var result = await useCase.CreateProject(command.Map(), cancellationToken);
var result = await useCase.CreateProject(command.Map(currentUser.Id), cancellationToken);

return result.Match<Results<Created<WebCreateProjectResponse>, Conflict<ProblemDetails>>>(
projectDetails =>
Expand Down
Loading
Loading