diff --git a/src/Balsam/.openapi-generator/VERSION b/src/Balsam/.openapi-generator/VERSION index 40e3636..ecb2186 100644 --- a/src/Balsam/.openapi-generator/VERSION +++ b/src/Balsam/.openapi-generator/VERSION @@ -1 +1 @@ -7.1.0-SNAPSHOT \ No newline at end of file +7.6.0-SNAPSHOT diff --git a/src/Balsam/Balsam.sln b/src/Balsam/Balsam.sln index 824ba16..6a41dd8 100644 --- a/src/Balsam/Balsam.sln +++ b/src/Balsam/Balsam.sln @@ -23,6 +23,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RocketChatChatProviderApiCl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BalsamApi.Server", "src\BalsamApi.Server\BalsamApi.Server.csproj", "{CCE90D11-C8D4-4F76-9730-506B23B1CD9E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Balsam.Application", "src\Balsam.Application\Balsam.Application.csproj", "{AAAA20E8-E584-401D-A7B4-6741E1316301}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Balsam.Domain", "src\Balsam.Domain\Balsam.Domain.csproj", "{AE263A3F-7988-4491-8339-89899DF6E08F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Balsam.Infrastructure", "src\Balsam.Infrastructure\Balsam.Infrastructure.csproj", "{98CB6D20-3EDC-4AA5-A477-83835FCD3EC7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Balsam.Tests", "src\Balsam.Tests\Balsam.Tests.csproj", "{FEE2F533-5550-4F4C-9FCB-A3695018307E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,6 +77,22 @@ Global {CCE90D11-C8D4-4F76-9730-506B23B1CD9E}.Debug|Any CPU.Build.0 = Debug|Any CPU {CCE90D11-C8D4-4F76-9730-506B23B1CD9E}.Release|Any CPU.ActiveCfg = Release|Any CPU {CCE90D11-C8D4-4F76-9730-506B23B1CD9E}.Release|Any CPU.Build.0 = Release|Any CPU + {AAAA20E8-E584-401D-A7B4-6741E1316301}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AAAA20E8-E584-401D-A7B4-6741E1316301}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AAAA20E8-E584-401D-A7B4-6741E1316301}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AAAA20E8-E584-401D-A7B4-6741E1316301}.Release|Any CPU.Build.0 = Release|Any CPU + {AE263A3F-7988-4491-8339-89899DF6E08F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE263A3F-7988-4491-8339-89899DF6E08F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE263A3F-7988-4491-8339-89899DF6E08F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE263A3F-7988-4491-8339-89899DF6E08F}.Release|Any CPU.Build.0 = Release|Any CPU + {98CB6D20-3EDC-4AA5-A477-83835FCD3EC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98CB6D20-3EDC-4AA5-A477-83835FCD3EC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98CB6D20-3EDC-4AA5-A477-83835FCD3EC7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98CB6D20-3EDC-4AA5-A477-83835FCD3EC7}.Release|Any CPU.Build.0 = Release|Any CPU + {FEE2F533-5550-4F4C-9FCB-A3695018307E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FEE2F533-5550-4F4C-9FCB-A3695018307E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FEE2F533-5550-4F4C-9FCB-A3695018307E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FEE2F533-5550-4F4C-9FCB-A3695018307E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Balsam/BalsamApi.Server.sln b/src/Balsam/BalsamApi.Server.sln index 0791abd..e516930 100644 --- a/src/Balsam/BalsamApi.Server.sln +++ b/src/Balsam/BalsamApi.Server.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.27428.2043 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BalsamApi.Server", "src\BalsamApi.Server\BalsamApi.Server.csproj", "{C7291B74-4521-4C9F-B7B4-92A1B686E169}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BalsamApi.Server", "src\BalsamApi.Server\BalsamApi.Server.csproj", "{5F5771EB-9749-4B0E-AB6C-028564A70FD6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,10 +11,10 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C7291B74-4521-4C9F-B7B4-92A1B686E169}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C7291B74-4521-4C9F-B7B4-92A1B686E169}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C7291B74-4521-4C9F-B7B4-92A1B686E169}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C7291B74-4521-4C9F-B7B4-92A1B686E169}.Release|Any CPU.Build.0 = Release|Any CPU + {5F5771EB-9749-4B0E-AB6C-028564A70FD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F5771EB-9749-4B0E-AB6C-028564A70FD6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F5771EB-9749-4B0E-AB6C-028564A70FD6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F5771EB-9749-4B0E-AB6C-028564A70FD6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Balsam/README.md b/src/Balsam/README.md index bf4f7b2..8a29fd4 100644 --- a/src/Balsam/README.md +++ b/src/Balsam/README.md @@ -6,6 +6,7 @@ This C# SDK is automatically generated by the [OpenAPI Generator](https://openap - API version: 2.0 - SDK version: 1.0.0 +- Generator version: 7.6.0-SNAPSHOT - Build package: org.openapitools.codegen.languages.CSharpClientCodegen diff --git a/src/Balsam/docs/GroupApi.md b/src/Balsam/docs/GroupApi.md index 3f000e1..9585a3e 100644 --- a/src/Balsam/docs/GroupApi.md +++ b/src/Balsam/docs/GroupApi.md @@ -6,6 +6,7 @@ All URIs are relative to *http://oidc-provider.balsam-system.svc.cluster.local/a |--------|--------------|-------------| | [**AddUserToGroup**](GroupApi.md#addusertogroup) | **POST** /groups/{groupId}/users | | | [**CreateGroup**](GroupApi.md#creategroup) | **POST** /groups | | +| [**DeleteGroup**](GroupApi.md#deletegroup) | **DELETE** /groups/{groupId} | | # **AddUserToGroup** @@ -185,3 +186,89 @@ No authorization required [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **DeleteGroup** +> void DeleteGroup (string groupId) + + + +Deletes a user group + +### Example +```csharp +using System.Collections.Generic; +using System.Diagnostics; +using OidcProviderApiClient.Api; +using OidcProviderApiClient.Client; +using OidcProviderApiClient.Model; + +namespace Example +{ + public class DeleteGroupExample + { + public static void Main() + { + Configuration config = new Configuration(); + config.BasePath = "http://oidc-provider.balsam-system.svc.cluster.local/api/v1"; + var apiInstance = new GroupApi(config); + var groupId = "groupId_example"; // string | The id for the group + + try + { + apiInstance.DeleteGroup(groupId); + } + catch (ApiException e) + { + Debug.Print("Exception when calling GroupApi.DeleteGroup: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); + } + } + } +} +``` + +#### Using the DeleteGroupWithHttpInfo variant +This returns an ApiResponse object which contains the response data, status code and headers. + +```csharp +try +{ + apiInstance.DeleteGroupWithHttpInfo(groupId); +} +catch (ApiException e) +{ + Debug.Print("Exception when calling GroupApi.DeleteGroupWithHttpInfo: " + e.Message); + Debug.Print("Status Code: " + e.ErrorCode); + Debug.Print(e.StackTrace); +} +``` + +### Parameters + +| Name | Type | Description | Notes | +|------|------|-------------|-------| +| **groupId** | **string** | The id for the group | | + +### Return type + +void (empty response body) + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/problem+json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +| **200** | Success | - | +| **400** | Error respsone for 400 | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/src/Balsam/src/Balsam.Api/Balsam.Api.csproj b/src/Balsam/src/Balsam.Api/Balsam.Api.csproj index 7c0227b..2165108 100644 --- a/src/Balsam/src/Balsam.Api/Balsam.Api.csproj +++ b/src/Balsam/src/Balsam.Api/Balsam.Api.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -9,22 +9,17 @@ - - - - + + + - - - - diff --git a/src/Balsam/src/Balsam.Api/Models/CapabilityOptions.cs b/src/Balsam/src/Balsam.Api/Configuration/ConfigureCapabilityOptions.cs similarity index 55% rename from src/Balsam/src/Balsam.Api/Models/CapabilityOptions.cs rename to src/Balsam/src/Balsam.Api/Configuration/ConfigureCapabilityOptions.cs index 69081f3..ce10757 100644 --- a/src/Balsam/src/Balsam.Api/Models/CapabilityOptions.cs +++ b/src/Balsam/src/Balsam.Api/Configuration/ConfigureCapabilityOptions.cs @@ -1,39 +1,24 @@ -using Microsoft.Extensions.Options; - -namespace Balsam.Api.Models -{ - public class CapabilityOptions - { - public bool Enabled { get; set; } - public string? ServiceLocation { get; set; } - public string[]? Actions { get; set; } - } - - public class Capabilities - { - public const string Git = "Git"; - public const string S3 = "S3"; - public const string Authentication = "Authentication"; - public const string Chat = "Chat"; - } - - public class ConfigureCapabilityOptions : IConfigureNamedOptions - { - - public ConfigureCapabilityOptions() - { - - } - - public void Configure(string name, CapabilityOptions options) - { - //options.ClientId = this.decrypt.Decrypt(options.ClientId); - //options.ClientSecret = this.decrypt.Decrypt(options.ClientSecret); - } - - public void Configure(CapabilityOptions options) - { - Configure(Options.DefaultName, options); - } - } -} +using Microsoft.Extensions.Options; + +namespace Balsam.Model +{ + public class ConfigureCapabilityOptions : IConfigureNamedOptions + { + + public ConfigureCapabilityOptions() + { + + } + + public void Configure(string name, CapabilityOptions options) + { + //options.ClientId = this.decrypt.Decrypt(options.ClientId); + //options.ClientSecret = this.decrypt.Decrypt(options.ClientSecret); + } + + public void Configure(CapabilityOptions options) + { + Configure(Options.DefaultName, options); + } + } +} diff --git a/src/Balsam/src/Balsam.Api/Controllers/KnowledgeLibraryController.cs b/src/Balsam/src/Balsam.Api/Controllers/KnowledgeLibraryController.cs index 6003c19..5e9058c 100644 --- a/src/Balsam/src/Balsam.Api/Controllers/KnowledgeLibraryController.cs +++ b/src/Balsam/src/Balsam.Api/Controllers/KnowledgeLibraryController.cs @@ -1,6 +1,8 @@ -using BalsamApi.Server.Models; +using Balsam.Api.Extensions; +using Balsam.Interfaces; +using Balsam.Model; +using BalsamApi.Server.Models; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; using System.ComponentModel.DataAnnotations; namespace Balsam.Api.Controllers @@ -9,25 +11,25 @@ namespace Balsam.Api.Controllers [ApiController] public class KnowledgeLibraryController : BalsamApi.Server.Controllers.KnowledgeLibraryApiController { - private readonly HubClient _hubClient; private readonly ILogger _logger; - private readonly KnowledgeLibraryClient _knowledgeLibraryClient; + private readonly IKnowledgeLibraryService _knowledgeLibraryService; - public KnowledgeLibraryController(ILogger logger, HubClient hubClient, KnowledgeLibraryClient knowledgeLibraryClient) + public KnowledgeLibraryController(ILogger logger, IKnowledgeLibraryService knowledgeLibraryService) { - _hubClient = hubClient; _logger = logger; - _knowledgeLibraryClient = knowledgeLibraryClient; + _knowledgeLibraryService = knowledgeLibraryService; } public async override Task ListKnowledgeLibaries() //A. Implementera { _logger.LogInformation("calling endpoint: Listing Knowledgelibraries"); try { - var knowledgeLibraries = await _hubClient.ListKnowledgeLibraries(); + var balsamKnowledgeLibraries = await _knowledgeLibraryService.ListKnowledgeLibraries(); + + var knowledgeLibraries = balsamKnowledgeLibraries.Select(bkl => bkl.ToKnowledgeLibrary()); return Ok(knowledgeLibraries); } - catch(Exception ex) + catch (Exception ex) { _logger.LogError(ex, "Error listing knowledgelibraries"); return BadRequest(ex); @@ -38,13 +40,16 @@ public async override Task GetKnowledgeLibraryFileContent([FromRo { try { + //TODO: Pull knowledge library repo + + //TODO: Move stream to knowledgeLibraryService? string filePath = string.Empty; - filePath = _knowledgeLibraryClient.GetRepositoryFilePath(libraryId, fileId); + filePath = _knowledgeLibraryService.GetRepositoryFilePath(libraryId, fileId); // Open the file var stream = System.IO.File.OpenRead(filePath); - + // Determine the content type var provider = new Microsoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider(); if (!provider.TryGetContentType(fileId, out var contentType)) @@ -69,16 +74,12 @@ public async override Task ListKnowledgeLibraryFiles([FromRoute(N { try { - var knowledgeLibrary = (await _hubClient.ListKnowledgeLibraries()).FirstOrDefault(kb => kb.Id == libraryId); + var balsamRepofiles = await _knowledgeLibraryService.GetRepositoryFileTree(libraryId); + var repoFiles = balsamRepofiles.Select(rf => rf.ToRepoFile()); - if (knowledgeLibrary is null) - { - return BadRequest(new Problem() { Status = 404, Title = "Knowledge library not found", Detail = "Knowledge library not found" }); - } - - var contents = _knowledgeLibraryClient.GetRepositoryContent(libraryId, knowledgeLibrary.RepositoryUrl); - return Ok(contents.ToArray()); - } catch (Exception ex) + return Ok(repoFiles.ToArray()); + } + catch (Exception ex) { _logger.LogError(ex, "Error listing knowledge library files"); return BadRequest(new Problem() { Status = 404, Title = "Error fetching knowledge libraries", Detail = "Error fetching knowledge libraries" }); diff --git a/src/Balsam/src/Balsam.Api/Controllers/ProjectController.cs b/src/Balsam/src/Balsam.Api/Controllers/ProjectController.cs index db9a846..156e609 100644 --- a/src/Balsam/src/Balsam.Api/Controllers/ProjectController.cs +++ b/src/Balsam/src/Balsam.Api/Controllers/ProjectController.cs @@ -1,11 +1,14 @@ -using Balsam.Api.Models; -using BalsamApi.Server.Models; +using BalsamApi.Server.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Authorization; -using LibGit2Sharp; using GitProviderApiClient.Api; +using Balsam.Model; +using Balsam.Interfaces; +using Balsam.Application.Authorization; +using System.Net; +using Balsam.Api.Extensions; namespace Balsam.Api.Controllers { @@ -15,84 +18,99 @@ namespace Balsam.Api.Controllers [Authorize] public class ProjectController : BalsamApi.Server.Controllers.ProjectApiController { - private readonly HubClient _hubClient; + private readonly IProjectService _projectService; private readonly ILogger _logger; - private readonly KnowledgeLibraryClient _knowledgeLibraryClient; + private readonly IKnowledgeLibraryService _knowledgeLibraryService; private readonly IRepositoryApi _repositoryApi; + private readonly ProjectAuthorization _projectAuthorization; - - public ProjectController(IOptionsSnapshot capabilityOptions, ILogger logger, HubClient hubClient, KnowledgeLibraryClient knowledgeLibraryClient, IRepositoryApi reposiotryApi) + public ProjectController(IOptionsSnapshot capabilityOptions, + ILogger logger, + IProjectService projectService, + IKnowledgeLibraryService knowledgeLibraryService, + IRepositoryApi reposiotryApi) { - _hubClient = hubClient; + _projectService = projectService; _logger = logger; capabilityOptions.Get(Capabilities.Git); capabilityOptions.Get(Capabilities.Authentication); - _knowledgeLibraryClient = knowledgeLibraryClient; + _knowledgeLibraryService = knowledgeLibraryService; _repositoryApi = reposiotryApi; + _projectAuthorization = new ProjectAuthorization(); //TODO: Use interface } public async override Task CreateBranch([FromRoute(Name = "projectId"), Required] string projectId, [FromBody] CreateBranchRequest? createBranchRequest) { if (createBranchRequest is null) { - return BadRequest(new Problem() { Status = 400, Title = "Parameter error", Detail = "Missing parameters" }); + return BadRequest(new Problem() { Status = (int)HttpStatusCode.BadRequest, Title = "Parameter error", Detail = "Missing parameters" }); } - BranchCreatedResponse branchCreatedResponse; try { - var branch = await _hubClient.CreateBranch(projectId, createBranchRequest.FromBranch, createBranchRequest.Name, createBranchRequest.Description); + var project = await _projectService.GetProject(projectId); + + if (!_projectAuthorization.CanUserCreateBranch(this.User, project)) + { + var username = this.User.GetUserName(); + _logger.LogInformation($"User {username} not authorized to create branch on project {project.Id}"); + return Unauthorized(new Problem() { Status = (int)HttpStatusCode.Unauthorized, Type = "Unauthorized", Title = "User cannot create branch" }); + } + + var branch = await _projectService.CreateBranch(projectId, createBranchRequest.FromBranch, createBranchRequest.Name, createBranchRequest.Description ?? ""); if (branch == null) { - return BadRequest(new Problem() { Status = 400, Title = "Could not create branch", Detail = "Branch could not be created" }); + return BadRequest(new Problem() { Status = (int)HttpStatusCode.BadRequest, Title = "Could not create branch", Detail = "Branch could not be created" }); } - branchCreatedResponse = new BranchCreatedResponse + var branchCreatedResponse = new BranchCreatedResponse { Id = branch.Id, Name = branch.Name, ProjectId = projectId }; + + return Ok(branchCreatedResponse); } catch (Exception ex) { _logger.LogError(ex, "Could not create branch"); - return BadRequest(new Problem() { Status = 400, Title = "Could not create branch", Detail = "Branch could not be created" }); + return BadRequest(new Problem() { Status = (int)HttpStatusCode.InternalServerError, Title = "Could not create branch", Detail = "Branch could not be created" }); } - - return Ok(branchCreatedResponse); } - public async override Task CreateProject([FromBody] CreateProjectRequest? createProjectRequest) { if (createProjectRequest is null) { - return BadRequest(new Problem() { Title = "Parameters missing", Status = 400, Type = "Missing parameters" }); + return BadRequest(new Problem() { Title = "Parameters missing", Status = (int)HttpStatusCode.BadRequest, Type = "Missing parameters" }); } - _logger.LogInformation("Reading user information"); - var username = this.User.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value; - _logger.LogInformation($"The user is {username}"); + + bool nameNotUnique = await _projectService.ProjectExists(createProjectRequest.Name); + + if (nameNotUnique) + { + return BadRequest(new Problem() { Title = "Project with that name already exists", Status = (int)HttpStatusCode.BadRequest, Type = "Project duplication" }); + } + try { - BalsamProject? project = await _hubClient.CreateProject(createProjectRequest.Name, createProjectRequest.Description, createProjectRequest.BranchName, username, createProjectRequest.SourceLocation); + var username = this.User.GetUserName(); - if (project == null) - { - return BadRequest(new Problem() { Title = "Project with that name already exists", Status = 400, Type = "Project duplication" }); - } + BalsamProject? project = await _projectService.CreateProject(createProjectRequest.Name, createProjectRequest.Description, createProjectRequest.BranchName, username, createProjectRequest.SourceLocation); - var evt = new ProjectCreatedResponse(); - evt.Id = project.Id; - evt.Name = project.Name; + var response = new ProjectCreatedResponse(); + response.Id = project.Id; + response.Name = project.Name; + //TODO: Set AuthGroup? - return Ok(evt); + return Ok(response); } catch (Exception ex) { - _logger.LogError("Could not create project", ex); - return BadRequest(new Problem() { Title = "Internal error", Status = 400, Type = "Internal error" }); + _logger.LogError(ex, "Could not create project"); + return BadRequest(new Problem() { Title = "Internal error", Status = (int)HttpStatusCode.InternalServerError, Type = "Internal error" }); } } @@ -100,140 +118,121 @@ public async override Task GetFiles([FromRoute(Name = "projectId" { try { - var files = await _hubClient.GetGitBranchFiles(projectId, branchId); - if (files is null) - { - return BadRequest(new Problem() { Status = 400, Type = "Fetch problem", Title = "Could not fetch files for repository branch" }); - } - return Ok(files.Select(f => new BalsamApi.Server.Models.RepoFile() - { - Name = f.Name, - Path = f.Path, - Type = f.Type == GitProviderApiClient.Model.RepoFile.TypeEnum.File ? BalsamApi.Server.Models.RepoFile.TypeEnum.FileEnum : BalsamApi.Server.Models.RepoFile.TypeEnum.FolderEnum, - ContentUrl = f.ContentUrl, - Id = f.Id - }).ToArray()); + var files = await _projectService.GetGitBranchFiles(projectId, branchId); + + return Ok(files.Select(f => f.ToRepoFile()).ToArray()); } catch (Exception ex) { _logger.LogError(ex, "Could not fetch files"); } - return BadRequest(new Problem() { Status = 400, Type = "Fetch problem", Title = "Could not fetch files for repository branch" }); + return BadRequest(new Problem() { Status = (int)HttpStatusCode.InternalServerError, Type = "Fetch problem", Title = "Could not fetch files for repository branch" }); } - public async override Task GetProject([FromRoute(Name = "projectId"), Required] string projectId) { try { - var balsamProject = await _hubClient.GetProject(projectId); - - if (balsamProject is null) - { - return BadRequest(new Problem() { Title = "Project with given id can not be found", Status = 400, Type = "Can not find project" }); - } + var project = await _projectService.GetProject(projectId); - var evt = new ProjectResponse(); - evt.Id = balsamProject.Id; - evt.Name = balsamProject.Name; - evt.Description = balsamProject.Description; - evt.GitUrl = balsamProject.Git is null ? "" : balsamProject.Git.Path; - evt.Branches = balsamProject.Branches.Select(b => new BalsamApi.Server.Models.Branch() { Id = b.Id, Description = b.Description, Name = b.Name, IsDefault = b.IsDefault }).ToList(); - evt.AuthGroup = balsamProject.Oidc.GroupName; + ProjectResponse response = project.ToProjectResponse(); - return Ok(evt); + return Ok(response); } catch (Exception ex) { - _logger.LogError("", ex); - return BadRequest(new Problem() { Title = "Project can not be loaded", Status = 400, Type = "Can not load project" }); + _logger.LogError(ex, "Could not get project"); + return BadRequest(new Problem() { Title = "Project can not be loaded", Status = (int)HttpStatusCode.BadRequest, Type = "Can not load project" }); } } - public override async Task ListProjects([FromQuery(Name = "all")] bool? all) { var listAll = all ?? true; _logger.LogDebug($"Hit ListProject viewAll={listAll}", listAll); - var projects = await _hubClient.GetProjects(); + var projects = await _projectService.GetProjects(); if (!listAll) { - var userGroups = User.Claims.Where(x => x.Type == "groups"); + var userGroups = User.GetGroups(); projects = projects.Where(x => userGroups.Any(o => string.Equals(o.Value, x.Oidc?.GroupName, StringComparison.OrdinalIgnoreCase))).ToList(); } var projectListResponse = new ProjectListResponse { - Projects = MapProject(projects) + Projects = projects.Select(project => project.ToProject()) + .OrderBy(p => p.Name) + .ToList() }; return Ok(projectListResponse); } - private List MapProject(List projects) - { - return projects.Select(project => new Project() - { - Id = project.Id, - Name = project.Name, - Description = project.Description, - Branches = MapBranches(project.Branches), - AuthGroup = project.Oidc.GroupName, - GitUrl = project.Git?.Path - }).OrderBy(p => p.Name).ToList(); - } - private List MapBranches(List branches) + public async override Task GetFile([FromRoute(Name = "projectId"), Required] string projectId, [FromRoute(Name = "branchId"), Required] string branchId, [FromRoute(Name = "fileId"), Required] string fileId) { - return branches.Select(branch => new BalsamApi.Server.Models.Branch() + try { - Id = branch.Id, - Description = branch.Description, - Name = branch.Name, - IsDefault = branch.IsDefault - }).ToList(); - } + //TODO: Authorize get content? + + var fileContent = await _projectService.GetFile(projectId, branchId, fileId); - public async override Task GetFile([FromRoute(Name = "projectId"), Required] string projectId, [FromRoute(Name = "branchId"), Required] string branchId, [FromRoute(Name = "fileId"), Required] string fileId) - { - var file = await _hubClient.GetFile(projectId, branchId, fileId); + if (fileContent.Content != null) + { + Response.Headers.Add("content-disposition", "inline"); - if (file != null) - { - Response.Headers.Add("content-disposition", "inline"); - return file; + var fileContentResult = new FileContentResult(fileContent.Content, fileContent.Mediatype); + return fileContentResult; + } + else + { + var errorMessage = "Could not read file content"; + _logger.LogError(errorMessage); + return NotFound(new Problem() { Status = (int)HttpStatusCode.NotFound, Type = "File read error", Detail = errorMessage }); + } } + catch (Exception ex) + { + var errorMessage = "Could not get file"; + _logger.LogError(ex, errorMessage); + return BadRequest(new Problem() { Status = (int)HttpStatusCode.InternalServerError, Type = "Get file error", Detail = errorMessage }); - return BadRequest(new Problem() { Status = 404, Type = "file not found", Detail = "Can not find the file" }); - + } } public async override Task DeleteBranch([FromRoute(Name = "projectId"), Required] string projectId, [FromRoute(Name = "branchId"), Required] string branchId) { try { - var project = await _hubClient.GetProject(projectId); - var branch = await _hubClient.GetBranch(projectId, branchId); - - if (project is null || branch is null) + var project = await _projectService.GetProject(projectId); + + if (project is null) { - return BadRequest(new Problem() { Status = 404, Type = "Project/branch not found", Detail = "Can not find the project/branch" }); + return NotFound(new Problem() { Status = (int)HttpStatusCode.NotFound, Type = "Project not found", Detail = "Can not find the project" }); + } + var branch = project.Branches.First(b => b.Id == branchId); + + if (project is null) + { + return NotFound(new Problem() { Status = (int)HttpStatusCode.NotFound, Type = "Branch not found", Detail = "Can not find the branch" }); } - else if (User.Claims.FirstOrDefault(x => x.Type == "groups" && x.Value == project.Oidc?.GroupName) is null) + + if (!_projectAuthorization.CanUserDeleteBranch(this.User, project)) { - return Unauthorized(new Problem() { Status = 401, Type = "Unauthorized", Detail = "User is not authorized to delete the branch" }); + var username = this.User.GetUserName(); + _logger.LogInformation($"User {username} not authorized to delete branch {branch.Id} on project {project.Id}"); + return Unauthorized(new Problem() { Status = (int)HttpStatusCode.Unauthorized, Type = "Unauthorized", Detail = "User is not authorized to delete the branch" }); } - await _hubClient.DeleteBranch(projectId, branchId); + await _projectService.DeleteBranch(projectId, branchId); } catch (Exception ex) { _logger.LogError(ex, "Could not delete branch"); - return BadRequest(new Problem() { Status = 400, Type = "Could not delete branch", Detail = "Could not delete branch, internal error" }); + return BadRequest(new Problem() { Status = (int)HttpStatusCode.InternalServerError, Type = "Could not delete branch", Detail = "Could not delete branch, internal error" }); } return Ok(); @@ -243,23 +242,26 @@ public async override Task DeleteProject([FromRoute(Name = "proje { try { - var project = await _hubClient.GetProject(projectId); + var project = await _projectService.GetProject(projectId); - if (project is null ) + if (project is null) { - return BadRequest(new Problem() { Status = 404, Type = "Project not found", Detail = "Can not find the project" }); - - } else if (User.Claims.FirstOrDefault(x => x.Type == "groups" && x.Value == project.Oidc?.GroupName) is null) + return NotFound(new Problem() { Status = (int)HttpStatusCode.NotFound, Type = "Project not found", Detail = "Can not find the project" }); + + } + else if (!_projectAuthorization.CanUserDeleteProject(this.User, project)) { - return Unauthorized(new Problem() { Status = 401, Type = "Unauthorized", Detail = "User is not authorized to delete the project" }); + var username = this.User.GetUserName(); + _logger.LogInformation($"User {username} not authorized to delete project {project.Id}"); + return Unauthorized(new Problem() { Status = (int)HttpStatusCode.Unauthorized, Type = "Unauthorized", Detail = "User is not authorized to delete the project" }); } - await _hubClient.DeleteProject(projectId); + await _projectService.DeleteProject(projectId); } catch (Exception ex) { _logger.LogError(ex, "Could not delete project"); - return BadRequest(new Problem() { Status = 400, Type = "Could not delete project", Detail = "Could not delete project, internal error" }); + return BadRequest(new Problem() { Status = (int)HttpStatusCode.InternalServerError, Type = "Could not delete project", Detail = "Could not delete project, internal error" }); } return Ok(); @@ -269,34 +271,37 @@ public async override Task CopyFromKnowleadgeLibrary([FromRoute(N { try { - var project = await _hubClient.GetProject(projectId); - var branch = await _hubClient.GetBranch(projectId, branchId); - if (project is null || branch is null) + var project = await _projectService.GetProject(projectId); + + if (project is null) + { + return NotFound(new Problem() { Status = (int)HttpStatusCode.NotFound, Type = "Project not found", Detail = "Can not find the project" }); + } + + if (!_projectAuthorization.CanUserEditBranch(this.User, project)) { - return BadRequest(new Problem() { Status = 404, Type = "Project/branch not found", Detail = "Can not find the project/branch" }); + var username = this.User.GetUserName(); + _logger.LogInformation($"User {username} not authorized to edit branch {branchId} on project {project.Id}"); + return Unauthorized(new Problem() { Status = (int)HttpStatusCode.Unauthorized, Type = "Unauthorized", Detail = "User is not authorized to edit project" }); } - var knowledgeLibrary = (await _hubClient.ListKnowledgeLibraries()).FirstOrDefault(kb => kb.Id == libraryId); + var knowledgeLibrary = _knowledgeLibraryService.GetKnowledgeLibrary(libraryId); if (knowledgeLibrary is null) { - return BadRequest(new Problem() { Status = 404, Title = "Knowledge library not found", Detail = "Knowledge library not found" }); + return NotFound(new Problem() { Status = (int)HttpStatusCode.NotFound, Title = "Knowledge library not found", Detail = "Knowledge library not found" }); } - var zipFile = _knowledgeLibraryClient.GetZippedResource(libraryId, knowledgeLibrary.RepositoryUrl, fileId); - - var stream = System.IO.File.OpenRead(zipFile); - await _repositoryApi.AddResourceFilesAsync(project.Git?.Id??"", branch.GitBranch, stream); - stream.Close(); - System.IO.File.Delete(zipFile); - + await _projectService.CopyFromKnowledgeLibrary(project.Id, branchId, libraryId, fileId); + _logger.LogInformation("File/directory copied from knowledge library"); + return Ok(); } catch (Exception ex) { _logger.LogError(ex, "Could not copy file/directory"); - return BadRequest(new Problem() { Status = 400, Title = "Could not copy file/directory", Detail = "Could not copy file/directory" }); + return BadRequest(new Problem() { Status = (int)HttpStatusCode.InternalServerError, Title = "Could not copy file/directory", Detail = "Could not copy file/directory" }); } } } diff --git a/src/Balsam/src/Balsam.Api/Controllers/WorkspaceController.cs b/src/Balsam/src/Balsam.Api/Controllers/WorkspaceController.cs index 6fccde8..6b811d9 100644 --- a/src/Balsam/src/Balsam.Api/Controllers/WorkspaceController.cs +++ b/src/Balsam/src/Balsam.Api/Controllers/WorkspaceController.cs @@ -1,9 +1,12 @@ -using Balsam.Api.Models; +using Balsam.Api.Extensions; +using Balsam.Application.Authorization; +using Balsam.Interfaces; +using Balsam.Model; using BalsamApi.Server.Models; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System.ComponentModel.DataAnnotations; +using System.Net; namespace Balsam.Api.Controllers { @@ -13,48 +16,54 @@ namespace Balsam.Api.Controllers public class WorkspaceController : BalsamApi.Server.Controllers.WorkspaceApiController { private readonly ILogger _logger; - private readonly HubClient _hubClient; + private readonly IProjectService _projectService; + private readonly IWorkspaceService _workspaceService; + private readonly WorkspaceAuthorization _workspaceAuthorization; - public WorkspaceController(ILogger logger, HubClient hubClient) + + public WorkspaceController(ILogger logger, IProjectService projectService, IWorkspaceService workspaceService) { _logger = logger; - _hubClient = hubClient; + _projectService = projectService; + _workspaceService = workspaceService; + _workspaceAuthorization = new WorkspaceAuthorization(); //TODO: Use interface } public async override Task CreateWorkspace([FromBody] CreateWorkspaceRequest? createWorkspaceRequest) { if (createWorkspaceRequest is null) { - return BadRequest(new Problem() { Status = 400, Title = "Missing parameters", Detail = "Missing input parameter(s)" }); + return BadRequest(new Problem() { Status = (int)HttpStatusCode.BadRequest, Title = "Missing parameters", Detail = "Missing input parameter(s)" }); } - var username = this.User.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value; - var mail = this.User.Claims.FirstOrDefault(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")?.Value; - var project = await _hubClient.GetProject(createWorkspaceRequest.ProjectId); - if (this.User.Claims.Any(c => c.Type == "groups" && c.Value == project.Oidc.GroupName)) + try { - try - { - var workspace = await _hubClient.CreateWorkspace(createWorkspaceRequest.ProjectId, createWorkspaceRequest.BranchId, createWorkspaceRequest.Name, createWorkspaceRequest.TemplateId, username, mail); + var project = await _projectService.GetProject(createWorkspaceRequest.ProjectId); - if (workspace == null) - { - return BadRequest(new Problem() { Status = 400, Type = "Could not create workspace", Title = "Could not create workspace due to error" }); - } + var username = this.User.GetUserName(); + var mail = this.User.GetEmail(); - return Ok(new WorkspaceCreatedResponse() { Id = workspace.Id, Name = workspace.Name, ProjectId = createWorkspaceRequest.ProjectId, BranchId = createWorkspaceRequest.BranchId, Url = workspace.Url }); + if (!_workspaceAuthorization.CanUserCreateWorkspace(this.User, project)) + { + _logger.LogInformation($"User {username} not authorized to crate workspace in project {project.Id}."); + return Unauthorized(new Problem() { Status = (int)HttpStatusCode.Unauthorized, Type = "Unauthorized", Title = "User cannot create workspace" }); } - catch (Exception ex) + + var workspace = await _workspaceService.CreateWorkspace(createWorkspaceRequest.ProjectId, createWorkspaceRequest.BranchId, createWorkspaceRequest.Name, createWorkspaceRequest.TemplateId, username, mail); + + if (workspace == null) { - _logger.LogError(ex, "Could not create workspace"); + return BadRequest(new Problem() { Status = (int)HttpStatusCode.BadRequest, Type = "Could not create workspace", Title = "Could not create workspace due to error" }); } + + return Ok(new WorkspaceCreatedResponse() { Id = workspace.Id, Name = workspace.Name, ProjectId = createWorkspaceRequest.ProjectId, BranchId = createWorkspaceRequest.BranchId, Url = workspace.Url }); } - else + catch (Exception ex) { - return BadRequest(new Problem() { Status = 403, Type = "User not in projectgroup", Title = "Internal error when created error" }); + _logger.LogError(ex, "Could not create workspace"); } - return BadRequest(new Problem() { Status = 400, Type = "Could not create workspace", Title = "Internal error when created error" }); - + + return BadRequest(new Problem() { Status = (int)HttpStatusCode.InternalServerError, Type = "Could not create workspace", Title = "Internal error when created error" }); } @@ -62,83 +71,95 @@ public async override Task DeleteWorkspace([FromRoute(Name = "wor { if (workspaceId is null) { - return BadRequest(new Problem() { Status = 400, Title = "Missing parameters", Detail = "Missing input parameter(s)" }); + return BadRequest(new Problem() { Status = (int)HttpStatusCode.BadRequest, Title = "Missing parameters", Detail = "Missing input parameter(s)" }); } - var userName = this.User.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value; + try { - var workspace = await _hubClient.DeleteWorkspace(projectId, branchId, workspaceId, userName); + var userName = this.User.GetUserName(); + var project = await _projectService.GetProject(projectId); + var workspace = await _workspaceService.GetWorkspace(projectId, userName, workspaceId); - if (workspace == null) + if (!_workspaceAuthorization.CanUserDeleteWorkspace(this.User, project, workspace)) { - return BadRequest(new Problem() { Status = 400, Type = "Could not delete workspace", Title = "Could not create workspace due to error" }); + _logger.LogInformation($"User {userName} not authorized to delete workspace {workspace.Id}."); + return Unauthorized(new Problem() { Status = ((int)HttpStatusCode.Unauthorized), Type = "Authentication error", Title = "User cannot delete workspace" }); } + await _workspaceService.DeleteWorkspace(projectId, branchId, workspaceId, userName); + return Ok($"workspace {workspaceId} deleted."); } catch (Exception ex) { _logger.LogError(ex, "Could not delete workspace"); + return BadRequest(new Problem() { Status = (int)HttpStatusCode.BadRequest, Type = "Could not delete workspace", Title = "Could not delete workspace due to error" }); } - - return BadRequest(new Problem() { Status = 400, Type = "Could not delete workspace", Title = "Internal error when created error" }); } - public override Task ListTemplates() + public async override Task ListTemplates() { - var workspaceTemplates = _hubClient.ListWorkspaceTemplates(); + var workspaceTemplates = await _workspaceService.ListWorkspaceTemplates(); var templates = workspaceTemplates - .Select(x => new Template { Id = x.Id, Name = x.Name, Description = x.Description }); + .Select(x => x.ToTemplate()); + if (templates.Any()) { - return Task.FromResult(Ok(templates)); + return await Task.FromResult(Ok(templates)); } - return Task.FromResult(BadRequest(new Problem + return BadRequest(new Problem { Detail = "No workspace templates found in hub repository", - Status = 417, - Title = "No workspace template found" - })); + Status = (int)HttpStatusCode.ExpectationFailed, + Title = "No workspace templates found" + }); } public async override Task ListWorkspaces([FromQuery(Name = "projectId")] string? projectId, [FromQuery(Name = "branchId")] string? branchId, [FromQuery(Name = "all")] bool? all) { + //TODO: Rename and inverse "all" to "ownedByUser" + if (branchId != null && projectId is null) { - return BadRequest(new Problem() { Status = 400, Type = "Parameter error", Title = "If BranchId is specified a ProjectId must also be specified"}); + return BadRequest(new Problem() { Status = (int)HttpStatusCode.BadRequest, Type = "Parameter error", Title = "If BranchId is specified a ProjectId must also be specified" }); } + List workspaces; if (projectId != null) { - if (all??true) + var username = this.User.GetUserName(); + var project = await _projectService.GetProject(projectId); + + //Removed authorization so that you can see workspaces for other project, but workspaces are still authorized elsewhere + //if (!_workspaceAuthorization.CanUserGetWorkspaces(this.User, project)) + //{ + // //TODO: Return empty list? + // _logger.LogInformation($"User {username} not authorized to get workspaces for project {project.Id}."); + // return Unauthorized(new Problem() { Status = (int)HttpStatusCode.Unauthorized, Type = "Not authorized", Title = "You are not a member of the project" }); + //} + + if (all ?? true) { if (branchId is null) { - workspaces = await _hubClient.GetWorkspacesByProject(projectId); + workspaces = await _workspaceService.GetWorkspacesByProject(projectId); } else { - workspaces = await _hubClient.GetWorkspacesByProjectAndBranch(projectId, branchId); + workspaces = await _workspaceService.GetWorkspacesByProjectAndBranch(projectId, branchId); } } else { - var project = await _hubClient.GetProject(projectId); - - if (!this.User.Claims.Any(c => c.Type == "groups" && c.Value == project.Oidc.GroupName) ) - { - return BadRequest(new Problem() { Status = 400, Type = "Not authorized", Title = "You are not a member of the project" }); - } - if (branchId is null) { - workspaces = await _hubClient.GetWorkspacesByProjectAndUser(projectId, this.User.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value); + workspaces = await _workspaceService.GetWorkspacesByProjectAndUser(projectId, username); } else { - workspaces = await _hubClient.GetWorkspacesByProjectBranchAndUser(projectId, branchId, this.User.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value); + workspaces = await _workspaceService.GetWorkspacesByProjectBranchAndUser(projectId, branchId, username); } } } @@ -146,17 +167,21 @@ public async override Task ListWorkspaces([FromQuery(Name = "proj { if (all ?? true) { - workspaces = await _hubClient.GetWorkspaces(); + workspaces = await _workspaceService.GetWorkspaces(); } else { - var projects = await _hubClient.GetProjects(false); - var groups = this.User.Claims.Where(c => c.Type == "groups").Select(c => c.Value).ToList(); + var projects = await _projectService.GetProjects(false); + var groups = this.User.GetGroups().Select(g => g.Value); + var userId = this.User.GetUserName(); var projectIds = projects.Where(p => groups.Contains(p.Oidc.GroupName)).Select(p => p.Id).ToList(); - workspaces = await _hubClient.GetWorkspacesByUser(this.User.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value, projectIds); + workspaces = await _workspaceService.GetWorkspacesByUser(userId, projectIds); } } - return Ok(workspaces.Select(w => new Workspace { Id = w.Id, Name = w.Name, ProjectId = w.ProjectId, BranchId = w.BranchId, TemplateId = w.TemplateId, Url = w.Url, Owner = w.Owner}).OrderBy(w => w.Name).ToArray()); + + return base.Ok(workspaces.Select(w => w.ToWorkspace()) + .OrderBy(w => w.Name).ToArray()); } + } } diff --git a/src/Balsam/src/Balsam.Api/Dockerfile b/src/Balsam/src/Balsam.Api/Dockerfile index 6d6c226..58e7d14 100644 --- a/src/Balsam/src/Balsam.Api/Dockerfile +++ b/src/Balsam/src/Balsam.Api/Dockerfile @@ -24,10 +24,14 @@ FROM mcr.microsoft.com/dotnet/sdk:6.0.400-1-alpine3.16-amd64 AS build WORKDIR /src COPY . . +RUN dotnet restore "Balsam.Domain/Balsam.Domain.csproj" +RUN dotnet restore "Balsam.Infrastructure/Balsam.Infrastructure.csproj" +RUN dotnet restore "Balsam.Application/Balsam.Application.csproj" RUN dotnet restore "Balsam.Api/Balsam.Api.csproj" WORKDIR "/src/Balsam.Api" -RUN dotnet build "Balsam.Api.csproj" -c Release -o /app/build +#TODO: Remove build because publish builds? +#RUN dotnet build "Balsam.Api.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "Balsam.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false diff --git a/src/Balsam/src/Balsam.Api/Extensions/BalsamBranchExtension.cs b/src/Balsam/src/Balsam.Api/Extensions/BalsamBranchExtension.cs new file mode 100644 index 0000000..bd2aaa6 --- /dev/null +++ b/src/Balsam/src/Balsam.Api/Extensions/BalsamBranchExtension.cs @@ -0,0 +1,19 @@ +using Balsam.Model; +using BalsamApi.Server.Models; + +namespace Balsam.Api.Extensions +{ + public static class BalsamBranchExtension + { + public static Branch ToBranch(this BalsamBranch b) + { + return new Branch() + { + Id = b.Id, + Description = b.Description, + Name = b.Name, + IsDefault = b.IsDefault + }; + } + } +} diff --git a/src/Balsam/src/Balsam.Api/Extensions/BalsamKnowledgeLibraryExtension.cs b/src/Balsam/src/Balsam.Api/Extensions/BalsamKnowledgeLibraryExtension.cs new file mode 100644 index 0000000..389fcd4 --- /dev/null +++ b/src/Balsam/src/Balsam.Api/Extensions/BalsamKnowledgeLibraryExtension.cs @@ -0,0 +1,20 @@ +using Balsam.Model; +using BalsamApi.Server.Models; + +namespace Balsam.Api.Extensions +{ + public static class BalsamKnowledgeLibraryExtension + { + public static KnowledgeLibrary ToKnowledgeLibrary(this BalsamKnowledgeLibrary bkl) + { + return new KnowledgeLibrary + { + Name = bkl.Name, + RepositoryUrl = bkl.RepositoryUrl, + Description = bkl.Description, + Id = bkl.Id, + RepositoryFriendlyUrl = bkl.RepositoryFriendlyUrl, + }; + } + } +} diff --git a/src/Balsam/src/Balsam.Api/Extensions/BalsamProjectExtension.cs b/src/Balsam/src/Balsam.Api/Extensions/BalsamProjectExtension.cs new file mode 100644 index 0000000..805fc6e --- /dev/null +++ b/src/Balsam/src/Balsam.Api/Extensions/BalsamProjectExtension.cs @@ -0,0 +1,37 @@ +using Balsam.Model; +using BalsamApi.Server.Models; +using System.Xml.Linq; + +namespace Balsam.Api.Extensions +{ + public static class BalsamProjectExtension + { + public static Project ToProject(this BalsamProject project) + { + return new Project() + { + Id = project.Id, + Name = project.Name, + Description = project.Description, + Branches = project.Branches.Select(b => b.ToBranch()).ToList(), + AuthGroup = project.Oidc?.GroupName, + GitUrl = project.Git?.Path + }; + } + + + //TODO: Do we need both Project and ProjectResponse? + public static ProjectResponse ToProjectResponse(this BalsamProject project) + { + return new ProjectResponse() + { + Id = project.Id, + Name = project.Name, + Description = project.Description, + Branches = project.Branches.Select(b => b.ToBranch()).ToList(), + AuthGroup = project.Oidc?.GroupName, + GitUrl = project.Git?.Path + }; + } + } +} diff --git a/src/Balsam/src/Balsam.Api/Extensions/BalsamRepoFileExtension.cs b/src/Balsam/src/Balsam.Api/Extensions/BalsamRepoFileExtension.cs new file mode 100644 index 0000000..ff17c38 --- /dev/null +++ b/src/Balsam/src/Balsam.Api/Extensions/BalsamRepoFileExtension.cs @@ -0,0 +1,33 @@ +using Balsam.Model; +using BalsamApi.Server.Models; + +namespace Balsam.Api.Extensions +{ + public static class BalsamRepoFileExtension + { + public static RepoFile ToRepoFile(this BalsamRepoFile repoFile) + { + var file = new RepoFile + { + Id = repoFile.Id, + Name = repoFile.Name, + Path = repoFile.Path, + ContentUrl = repoFile.ContentUrl, + }; + + switch (repoFile.Type) + { + case BalsamRepoFile.TypeEnum.FileEnum: + file.Type = RepoFile.TypeEnum.FileEnum; + break; + case BalsamRepoFile.TypeEnum.FolderEnum: + file.Type = RepoFile.TypeEnum.FolderEnum; + break; + default: + throw new ApplicationException("Could not convert type " + repoFile.Type.ToString()); + } + + return file; + } + } +} diff --git a/src/Balsam/src/Balsam.Api/Extensions/BalsamWorkspaceExtension.cs b/src/Balsam/src/Balsam.Api/Extensions/BalsamWorkspaceExtension.cs new file mode 100644 index 0000000..a14a1d5 --- /dev/null +++ b/src/Balsam/src/Balsam.Api/Extensions/BalsamWorkspaceExtension.cs @@ -0,0 +1,22 @@ +using Balsam.Model; +using BalsamApi.Server.Models; + +namespace Balsam.Api.Extensions +{ + public static class BalsamWorkspaceExtension + { + public static Workspace ToWorkspace(this BalsamWorkspace w) + { + return new Workspace + { + Id = w.Id, + Name = w.Name, + ProjectId = w.ProjectId, + BranchId = w.BranchId, + TemplateId = w.TemplateId, + Url = w.Url, + Owner = w.Owner + }; + } + } +} diff --git a/src/Balsam/src/Balsam.Api/Extensions/ClaimsExtension.cs b/src/Balsam/src/Balsam.Api/Extensions/ClaimsExtension.cs new file mode 100644 index 0000000..100b549 --- /dev/null +++ b/src/Balsam/src/Balsam.Api/Extensions/ClaimsExtension.cs @@ -0,0 +1,21 @@ +using System.Security.Claims; + +namespace Balsam.Api.Extensions +{ + public static class ClaimsExtension + { + public static string GetUserName(this ClaimsPrincipal claimsPrincipal) + { + return claimsPrincipal.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value; + } + public static List GetGroups(this ClaimsPrincipal claimsPrincipal) + { + return claimsPrincipal.Claims.Where(x => x.Type == "groups").ToList(); + } + + public static string GetEmail(this ClaimsPrincipal claimsPrincipal) + { + return claimsPrincipal.Claims.FirstOrDefault(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")?.Value; + } + } +} diff --git a/src/Balsam/src/Balsam.Api/Extensions/RepoFileExtension.cs b/src/Balsam/src/Balsam.Api/Extensions/RepoFileExtension.cs new file mode 100644 index 0000000..e812f3e --- /dev/null +++ b/src/Balsam/src/Balsam.Api/Extensions/RepoFileExtension.cs @@ -0,0 +1,35 @@ +using Balsam.Model; +using GitProviderApiClient.Model; +using System.Runtime.CompilerServices; + +namespace Balsam.Api.Extensions +{ + public static class RepoFileExtension + { + public static BalsamRepoFile ToBalsamRepoFile(this RepoFile repoFile) + { + var balsamRepoFile = new BalsamRepoFile + { + Id = repoFile.Id, + ContentUrl = repoFile.ContentUrl, + Name = repoFile.Name, + Path = repoFile.Path, + + }; + + switch (repoFile.Type) + { + case RepoFile.TypeEnum.File: + balsamRepoFile.Type = BalsamRepoFile.TypeEnum.FileEnum; + break; + case RepoFile.TypeEnum.Folder: + balsamRepoFile.Type = BalsamRepoFile.TypeEnum.FolderEnum; + break; + default: + throw new NotImplementedException(); + } + + return balsamRepoFile; + } + } +} diff --git a/src/Balsam/src/Balsam.Api/Extensions/WorkspaceTemplateExtension.cs b/src/Balsam/src/Balsam.Api/Extensions/WorkspaceTemplateExtension.cs new file mode 100644 index 0000000..5dd6bde --- /dev/null +++ b/src/Balsam/src/Balsam.Api/Extensions/WorkspaceTemplateExtension.cs @@ -0,0 +1,18 @@ +using Balsam.Model; +using BalsamApi.Server.Models; + +namespace Balsam.Api.Extensions +{ + public static class WorkspaceTemplateExtension + { + public static Template ToTemplate(this WorkspaceTemplate workspaceTemplate) + { + return new Template + { + Id = workspaceTemplate.Id, + Name = workspaceTemplate.Name, + Description = workspaceTemplate.Description + }; + } + } +} diff --git a/src/Balsam/src/Balsam.Api/GitClient.cs b/src/Balsam/src/Balsam.Api/GitClient.cs deleted file mode 100644 index c96ff10..0000000 --- a/src/Balsam/src/Balsam.Api/GitClient.cs +++ /dev/null @@ -1,49 +0,0 @@ -//using Balsam.Api.Models; -//using Microsoft.Extensions.Options; -//using Newtonsoft.Json; -//using System.Net.Http.Headers; - -//namespace Balsam.Api -//{ -// public class GitClient -// { -// private HttpClient _httpClient; - -// //private string _baseUrl = "http://git-provider.balsam-system.svc.cluster.local/api/v1"; -// private string _baseUrl = "http://localhost:8081/api/v1"; -// public GitClient(IOptionsSnapshot capabilityOptions, HttpClient httpClient) -// { -// var gitOptions = capabilityOptions.Get(Capabilities.Git); -// _baseUrl = gitOptions.ServiceLocation; -// _httpClient = httpClient; - -// httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); -// httpClient.Timeout = TimeSpan.FromSeconds(30); -// } - - -// public async Task CreateRepository(string repositoryName) -// { -// HttpResponseMessage response = await _httpClient.PostAsync($"{_baseUrl}/repos?preferredName={repositoryName}", null); -// response.EnsureSuccessStatusCode(); - -// GitData data = new GitData(); - -// if (response.Content is object && response.Content.Headers.ContentType != null && response.Content.Headers.ContentType.MediaType == "application/json") -// { -// string content = await response.Content.ReadAsStringAsync(); -// if (content == null) -// { -// return null; -// } -// dynamic createResponse = JsonConvert.DeserializeObject(content); -// if (createResponse != null) -// { -// data.Name = createResponse.name; -// data.Path = createResponse.path; -// } -// } -// return data; -// } -// } -//} \ No newline at end of file diff --git a/src/Balsam/src/Balsam.Api/HubClient.cs b/src/Balsam/src/Balsam.Api/HubClient.cs deleted file mode 100644 index 55a7c2d..0000000 --- a/src/Balsam/src/Balsam.Api/HubClient.cs +++ /dev/null @@ -1,836 +0,0 @@ -using Balsam.Api.Models; -using GitProviderApiClient.Api; -using GitProviderApiClient.Model; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using S3ProviderApiClient.Api; -using S3ProviderApiClient.Model; -using OidcProviderApiClient.Api; -using OidcProviderApiClient.Model; -using System.Text.RegularExpressions; -using System.IO.Hashing; -using RocketChatChatProviderApiClient.Api; -using HandlebarsDotNet; -using File = System.IO.File; -using Microsoft.AspNetCore.Mvc; -using BalsamApi.Server.Models; -using LibGit2Sharp; - -namespace Balsam.Api -{ - public class HubClient - { - private readonly CapabilityOptions _git; - private readonly CapabilityOptions _s3; - private readonly CapabilityOptions _authentication; - private readonly CapabilityOptions _chat; - private readonly IBucketApi _s3Client; - private readonly IGroupApi _oidcClient; - private readonly HubRepositoryClient _hubRepositoryClient; - private readonly IMemoryCache _memoryCache; - private readonly IRepositoryApi _repositoryApi; - private readonly ILogger _logger; - private readonly IAreaApi _chatClient; - private readonly IUserApi _gitUserClient; - - - public HubClient(ILogger logger, IOptionsSnapshot capabilityOptions, IMemoryCache memoryCache, HubRepositoryClient hubRepoClient, IBucketApi s3Client, IRepositoryApi reposiotryApi, IGroupApi oidcClient, IAreaApi chatClient, IUserApi gitUserClient) - { - _logger = logger; - _memoryCache = memoryCache; - _s3Client = s3Client; - _oidcClient = oidcClient; - _chatClient = chatClient; - _gitUserClient = gitUserClient; - - _hubRepositoryClient = hubRepoClient; - - _git = capabilityOptions.Get(Capabilities.Git); - _s3 = capabilityOptions.Get(Capabilities.S3); - _authentication = capabilityOptions.Get(Capabilities.Authentication); - _chat = capabilityOptions.Get(Capabilities.Chat); - _repositoryApi = reposiotryApi; - - } - - static HubClient() - { - Handlebars.RegisterHelper("curlies", (writer, context, parameters) => - { - if (parameters.Length == 1 && parameters.At(0) == true) - { - writer.Write("{{"); - } - else - { - writer.Write("}}"); - } - - }); - } - - public async Task> GetProjects(bool includeBranches = true) - { - var projects = new List(); - var hubPath = Path.Combine(_hubRepositoryClient.Path, "hub"); - - foreach (var projectPath in Directory.GetDirectories(hubPath)) - { - var propsFile = Path.Combine(projectPath, "properties.json"); - var project = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); - if (project != null) - { - if (includeBranches) - { - project.Branches = await ReadBranches(projectPath); - } - projects.Add(project); - } - else - { - _logger.LogWarning($"Could not parse properties file {propsFile}"); - } - } - return projects; - } - - public async Task GetProject(string projectId, bool includeBranches = true) - { - var projectPath = Path.Combine(_hubRepositoryClient.Path, "hub", projectId); - var propsFile = Path.Combine(projectPath, "properties.json"); - - if (!File.Exists(propsFile)) - { - return null; - } - var project = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); - if (project != null && includeBranches) - { - project.Branches = await ReadBranches(projectPath); - } - return project; - } - - public async Task GetProject(string projectId) - { - var projectPath = Path.Combine(_hubRepositoryClient.Path, "hub", projectId); - - if (!Directory.Exists(projectPath)) - { - return null; - } - - var propsFile = Path.Combine(projectPath, "properties.json"); - var project = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); - if (project != null) - { - project.Branches = await ReadBranches(projectPath); - } - - return project; - } - - private async Task> ReadBranches(string projectPath) - { - var branches = new List(); - - if (!Directory.Exists(projectPath)) - { - return branches; - } - - foreach (var branchPath in Directory.GetDirectories(projectPath)) - { - var propsFile = Path.Combine(branchPath, "properties.json"); - var branch = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); - if (branch != null) - { - branches.Add(branch); - } - else - { - _logger.LogWarning($"Could not parse properties file {propsFile}"); - } - } - return branches; - } - - public async Task GetBranch(string projectId, string branchId) - { - var propsFile = Path.Combine(_hubRepositoryClient.Path, "hub", projectId, branchId, "properties.json"); - - if (!File.Exists(propsFile)) - { - return null; - } - - var branch = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); - return branch; - } - - private async Task ProjectExists(string projectName) - { - var projects = await GetProjects(false); - if (projects.FirstOrDefault(p => p.Name == projectName) == null) - { - return false; - } - return true; - } - - public async Task CreateProject(string preferredName, string description, string defaultBranchName, string username, string? sourceLocation) - { - //Check if there is a program with the same name. - _logger.LogDebug("Check for duplicate names"); - if (await ProjectExists(preferredName)) - { - _logger.LogInformation($"Could not create project {preferredName}, due to name duplication"); - return null; - } - - _hubRepositoryClient.PullChanges(); - - _logger.LogDebug($"create project information"); - var project = new BalsamProject(SanitizeName(preferredName), preferredName, description); - string projectPath = Path.Combine(_hubRepositoryClient.Path, "hub", project.Id); - - _logger.LogDebug($"Assure path exists {projectPath}"); - - DirectoryUtil.AssureDirectoryExists(projectPath); - - _logger.LogDebug($"Begin call service providers"); - - if (_authentication.Enabled) - { - _logger.LogDebug($"Begin call OpenIdConnect"); - var oidcData = await _oidcClient.CreateGroupAsync(new CreateGroupRequest(project.Id)); - await _oidcClient.AddUserToGroupAsync(oidcData.Id, new AddUserToGroupRequest(username)); - project.Oidc = new OidcData(oidcData.Id, oidcData.Name); - _logger.LogInformation($"Group {project.Oidc.GroupName}({project.Oidc.GroupId}) created"); - } - - if (project.Oidc == null) - { - throw new Exception("Could not parse oidc data"); - } - - if (_git.Enabled) - { - _logger.LogDebug($"Begin call Git"); - var gitData = await _repositoryApi.CreateRepositoryAsync(new CreateRepositoryRequest(preferredName, description, defaultBranchName)); - defaultBranchName = gitData.DefaultBranchName; - project.Git = new GitData() { Id = gitData.Id, Name = gitData.Name, Path = gitData.Path, SourceLocation = sourceLocation }; - _logger.LogInformation($"Git repository {project.Git.Name} created"); - } - - if (_s3.Enabled) - { - _logger.LogDebug($"Begin call S3"); - var s3Data = await _s3Client.CreateBucketAsync(new CreateBucketRequest(preferredName, project.Oidc.GroupName)); - project.S3 = new S3Data() { BucketName = s3Data.Name }; - _logger.LogInformation($"Bucket {project.S3.BucketName} created"); - } - - if (_chat.Enabled) - { - _logger.LogDebug("Begin the call to chatprovider"); - var chatData = await _chatClient.CreateAreaAsync(new RocketChatChatProviderApiClient.Model.CreateAreaRequest(preferredName)); - project.Chat = new ChatData(chatData.Id, chatData.Name); - _logger.LogInformation($"Channel created named {chatData.Name}"); - } - - string propPath = Path.Combine(projectPath, "properties.json"); - - if (await CreateBranch(project, defaultBranchName, description, true) != null) - { - _logger.LogInformation($"Default Balsam branch {defaultBranchName} created"); - } - - // serialize JSON to a string and then write string to a file - await File.WriteAllTextAsync(propPath, JsonConvert.SerializeObject(project)); - - await CreateProjectManifests(project, projectPath); - _hubRepositoryClient.PersistChanges($"New program with id {project.Id}"); - _logger.LogInformation($"Project {project.Name}({project.Id}) created"); - return project; - } - - public async Task CreateWorkspace(string projectId, string branchId, string name, string templateId, string userName, string userMail) - { - var branchPath = Path.Combine(_hubRepositoryClient.Path, "hub", projectId, branchId); - - if (!Directory.Exists(branchPath)) - { - return null; - } - - var workspace = new BalsamWorkspace(CreateWorkspaceId(name), name, templateId, projectId, branchId, userName); - - var workspacePath = Path.Combine(branchPath, userName, workspace.Id); - - DirectoryUtil.AssureDirectoryExists(workspacePath); - - - var project = await GetProject(projectId); - var branch = project.Branches.FirstOrDefault(b => b.Id == branchId); - var gitPAT = string.Empty; - - if (_git.Enabled) - { - var patResponse = await _gitUserClient.CreatePATAsync(userName); - gitPAT = patResponse.Token; - _logger.LogInformation($"Git PAT created"); - } - - var user = new UserInfo(userName, userMail, gitPAT); - var propPath = Path.Combine(workspacePath, "properties.json"); - - _logger.LogDebug("Pulling changes"); - _hubRepositoryClient.PullChanges(); - // serialize JSON to a string and then write string to a file - await CreateWorkspaceManifests(project, branch, workspace, user, workspacePath, templateId); - - var template = await GetWorkspaceTemplate(templateId); - workspace.Url = ManifestUtil.GetWorkspaceUrl(Path.Combine(workspacePath, template.UrlConfig)); - await File.WriteAllTextAsync(propPath, JsonConvert.SerializeObject(workspace)); - - _hubRepositoryClient.PersistChanges($"New workspace with id {project.Id}"); - _logger.LogInformation("Workspace created"); - return workspace; - } - - public async Task DeleteWorkspace(string projectId, string branchId, string workspaceId, string userName) - { - _hubRepositoryClient.PullChanges(); - var workspacePath = Path.Combine(_hubRepositoryClient.Path, "hub", projectId, branchId, userName, workspaceId); - - if (!Directory.Exists(workspacePath)) - { - return null; - } - - if (Directory.Exists(workspacePath)) - { - EmptyDirectory(workspacePath); - Directory.Delete(workspacePath, true); - } - - _hubRepositoryClient.PersistChanges($"Deleted workspace with id {workspaceId}"); - return "workspace deleted"; - } - - private async Task CreateWorkspaceManifests(BalsamProject project, BalsamBranch branch, BalsamWorkspace workspace, UserInfo user, string workspacePath, string templateId) - { - var token = new AccessKeyCreatedResponse("", ""); - if (_s3.Enabled) - { - token = await _s3Client.CreateAccessKeyAsync(project.S3.BucketName); - } - - var s3Token = new S3Token(token.AccessKey, token.SecretKey); - user.S3 = s3Token; - var context = new WorkspaceContext(project, branch, workspace, user); - await CreateManifests(context, workspacePath, "workspaces" + Path.DirectorySeparatorChar + templateId); - } - - private async Task CreateProjectManifests(BalsamProject project, string projectPath) - { - var context = new ProjectContext() { Project = project }; - - await CreateManifests(context, projectPath, "projects"); - } - - private async Task CreateManifests(BalsamContext context, string destinationPath, string templateName) - { - var templatePath = Path.Combine(_hubRepositoryClient.Path, "templates", templateName); - - foreach (var file in Directory.GetFiles(templatePath, "*.yaml")) - { - var source = await File.ReadAllTextAsync(file); - - var template = Handlebars.Compile(source); - - var result = template(context); - - var destinationFilePath = Path.Combine(destinationPath, Path.GetFileName(file)); - - await File.WriteAllTextAsync(destinationFilePath, result); - } - - } - - private async Task GetWorkspaceTemplate(string templateId) - { - var templatePath = Path.Combine(_hubRepositoryClient.Path, "templates", "workspaces", templateId, "properties.json"); - - if (!File.Exists(templatePath)) return null; - - return JsonConvert.DeserializeObject(await File.ReadAllTextAsync(templatePath)); - } - - public async Task CreateBranch(string projectId, string fromBranch, string branchName, string description) - { - var project = await GetProject(projectId, false); - var branch = await GetBranch(projectId, fromBranch); - - if (project is null || project.Git is null || branch is null || branch.GitBranch is null) - { - return null; - } - - var response = await _repositoryApi.CreateBranchAsync(project.Git.Id, new GitProviderApiClient.Model.CreateBranchRequest(branchName, branch.GitBranch)); - branchName = response.Name; - - _hubRepositoryClient.PullChanges(); - - var createdBranch = await CreateBranch(project, branchName, description, false); - _hubRepositoryClient.PersistChanges($"Branch {branchName} created for project {project.Name}"); - - return createdBranch; - } - - private async Task CreateBranch(BalsamProject project, string branchName, string description, bool isDefault = false) - { - var branchId = SanitizeName(branchName); - var branchPath = Path.Combine(_hubRepositoryClient.Path, "hub", project.Id, branchId); - - DirectoryUtil.AssureDirectoryExists(branchPath); - - if (project.S3 is null || string.IsNullOrEmpty(project.S3.BucketName)) - { - return null; - } - - if (_s3.Enabled) - { - await _s3Client.CreateFolderAsync(project.S3.BucketName, new CreateFolderRequest(branchName)); - _logger.LogInformation($"Folder {branchName} created in bucket {project.S3.BucketName}."); - } - - var branch = new BalsamBranch() - { - Id = branchId, - Name = branchName, - Description = description, - IsDefault = isDefault, - GitBranch = branchName - }; - - var propPath = Path.Combine(branchPath, "properties.json"); - await File.WriteAllTextAsync(propPath, JsonConvert.SerializeObject(branch)); - - return branch; - } - - public async Task?> GetGitBranchFiles(string projectId, string branchId) - { - var project = await GetProject(projectId); - var branch = await GetBranch(projectId, branchId); - if (project is null || branch is null || project.Git is null) - { - return null; - } - return _repositoryApi.GetFilesInBranch(project.Git.Id, branch.Name); - } - - public async Task GetFile(string projectId, string branchId, string fileId) - { - var project = await GetProject(projectId); - var branch = await GetBranch(projectId, branchId); - if (project is null || branch is null || project.Git is null) - { - return null; - } - - try - { - var request = new HttpRequestMessage(HttpMethod.Get, $"{_git.ServiceLocation}/repos/{project.Git.Id}/branches/{branch.GitBranch}/files/{fileId}"); - - var httpClient = new HttpClient(); - - var response = await httpClient.SendAsync(request); - if (response.IsSuccessStatusCode) - { - var data = await response.Content.ReadAsByteArrayAsync(); - return new FileContentResult(data, response.Content.Headers.ContentType?.MediaType ?? "application/octet-stream"); - } - - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not read files from repository"); - } - - return null; - } - - - private string SanitizeName(string name) - { - var crc32 = new Crc32(); - - crc32.Append(System.Text.Encoding.ASCII.GetBytes(name)); - var hash = crc32.GetCurrentHash(); - var crcHash = string.Join("", hash.Select(b => b.ToString("x2").ToLower()).Reverse()); - - name = name.ToLower(); //Only lower charachters allowed - name = name.Replace(" ", "-"); //replaces spaches with hypen - name = Regex.Replace(name, @"[^a-z0-9\-]", ""); // make sure that only a-z or digit or hypen removes all other characters - name = name.Substring(0, Math.Min(50 - crcHash.Length, name.Length)) + "-" + crcHash; //Assures max size of 50 characters - - return name; - - } - - private static void EmptyDirectory(string directory) - { - var di = new DirectoryInfo(directory); - foreach (var file in di.GetFiles()) - { - file.Delete(); - } - foreach (var dir in di.GetDirectories()) - { - dir.Delete(true); - } - } - - private static string CreateWorkspaceId(string name) - { - var crc32 = new Crc32(); - - crc32.Append(System.Text.Encoding.ASCII.GetBytes(name + Guid.NewGuid().ToString())); - var hash = crc32.GetCurrentHash(); - var crcHash = string.Join("", hash.Select(b => b.ToString("x2").ToLower()).Reverse()); - - name = name.ToLower(); //Only lower charachters allowed - name = name.Replace(" ", "-"); //replaces spaches with hypen - name = Regex.Replace(name, @"[^a-z0-9\-]", ""); // make sure that only a-z or digit or hypen removes all other characters - name = name.Substring(0, Math.Min(50 - crcHash.Length, name.Length)) + "-" + crcHash; //Assures max size of 50 characters - - return name; - } - - public IEnumerable ListWorkspaceTemplates() - { - _logger.LogDebug("Start ListWorkspaceTemplates"); - var workspaceTemplatePath = Path.Combine(_hubRepositoryClient.Path, "templates", "workspaces"); - - var workspaceTemplates = new List(); - - if (!Directory.Exists(workspaceTemplatePath)) - { - _logger.LogInformation("No workspace template folder found!"); - return workspaceTemplates; - } - - foreach (var directory in Directory.GetDirectories(workspaceTemplatePath)) - { - var id = new DirectoryInfo(directory).Name; - var fileName = Path.Combine(directory, "properties.json"); - var jsonString = File.ReadAllText(fileName); - var template = JsonConvert.DeserializeObject(jsonString); - - if (template == null) continue; - - template.Id = id; - workspaceTemplates.Add(template); - } - - _logger.LogDebug("End ListWorkspaceTemplates"); - return workspaceTemplates; - } - - public async Task> GetWorkspaces() - { - var hubPath = Path.Combine(_hubRepositoryClient.Path, "hub"); - - var workspaces = new List(); - - foreach (var projectPath in Directory.GetDirectories(hubPath)) - { - foreach (var branchPath in Directory.GetDirectories(projectPath)) - { - foreach (var userPath in Directory.GetDirectories(branchPath)) - { - foreach (var workspacePath in Directory.GetDirectories(userPath)) - { - var propsFile = Path.Combine(workspacePath, "properties.json"); - var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); - - if (workspace == null) continue; - workspaces.Add(workspace); - } - } - } - } - return workspaces; - } - - public async Task> GetWorkspacesByUser(string userId, List projectIds) - { - var hubPath = Path.Combine(_hubRepositoryClient.Path, "hub"); - - var workspaces = new List(); - - foreach (var projectId in projectIds) - { - var projectPath = Path.Combine(hubPath, projectId); - if (Directory.Exists(projectPath)) - { - foreach (var branchPath in Directory.GetDirectories(projectPath)) - { - var userPath = Path.Combine(branchPath, userId); - - if (Directory.Exists(userPath)) - { - foreach (var workspacePath in Directory.GetDirectories(userPath)) - { - var propsFile = Path.Combine(workspacePath, "properties.json"); - var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); - - if (workspace == null) continue; - workspaces.Add(workspace); - } - } - } - } - } - return workspaces; - } - - public async Task> GetWorkspacesByProject(string projectId) - { - var hubPath = Path.Combine(_hubRepositoryClient.Path, "hub"); - - var workspaces = new List(); - - var projectPath = Path.Combine(hubPath, projectId); - - if (Directory.Exists(projectPath)) - { - foreach (var branchPath in Directory.GetDirectories(projectPath)) - { - foreach (var userPath in Directory.GetDirectories(branchPath)) - { - foreach (var workspacePath in Directory.GetDirectories(userPath)) - { - var propsFile = Path.Combine(workspacePath, "properties.json"); - var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); - - if (workspace == null) continue; - workspaces.Add(workspace); - } - } - } - } - return workspaces; - } - - public async Task> GetWorkspacesByProjectAndBranch(string projectId, string branchId) - { - var hubPath = Path.Combine(_hubRepositoryClient.Path, "hub"); - - var workspaces = new List(); - - var branchPath = Path.Combine(hubPath, projectId, branchId); - - if (Directory.Exists(branchPath)) - { - foreach (var userPath in Directory.GetDirectories(branchPath)) - { - foreach (var workspacePath in Directory.GetDirectories(userPath)) - { - var propsFile = Path.Combine(workspacePath, "properties.json"); - var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); - - if (workspace == null) continue; - workspaces.Add(workspace); - } - } - } - return workspaces; - } - - public async Task> GetWorkspacesByProjectAndUser(string projectId, string userId) - { - var hubPath = Path.Combine(_hubRepositoryClient.Path, "hub"); - - var workspaces = new List(); - - var projectPath = Path.Combine(hubPath, projectId); - - if (Directory.Exists(projectPath)) - { - foreach (var branchPath in Directory.GetDirectories(projectPath)) - { - var userPath = Path.Combine(branchPath, userId); - if (Directory.Exists(userPath)) - { - foreach (var workspacePath in Directory.GetDirectories(userPath)) - { - var propsFile = Path.Combine(workspacePath, "properties.json"); - var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); - - if (workspace == null) continue; - workspaces.Add(workspace); - } - } - } - } - return workspaces; - } - - public async Task> GetWorkspacesByProjectBranchAndUser(string projectId, string branchId, string userId) - { - var hubPath = Path.Combine(_hubRepositoryClient.Path, "hub"); - - var workspaces = new List(); - - var userPath = Path.Combine(hubPath, projectId, branchId, userId); - - if (Directory.Exists(userPath)) - { - foreach (var workspacePath in Directory.GetDirectories(userPath)) - { - var propsFile = Path.Combine(workspacePath, "properties.json"); - var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); - - if (workspace == null) continue; - workspaces.Add(workspace); - } - } - return workspaces; - } - - public async Task DeleteBranch(string projectId, string branchId) - { - var branchPath = Path.Combine(_hubRepositoryClient.Path, "hub", projectId, branchId); - - var project = await GetProject(projectId); - var branch = await GetBranch(projectId, branchId); - - //Asure that the id are correct - if (project == null || branch == null) return; - - - if (_s3.Enabled) - { - try - { - await _s3Client.DeleteFolderAsync(project.S3?.BucketName??"", branch.Name); - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not delete s3 folder"); - } - } - - if (_git.Enabled) - { - try - { - await _repositoryApi.DeleteRepositoryBranchAsync(project.Git?.Id ?? "", branch.GitBranch); - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not delete git branch"); - } - } - - - _hubRepositoryClient.PullChanges(); - - if (Directory.Exists(branchPath)) - { - //EmptyDirectory(branchPath); - Directory.Delete(branchPath, true); - } - - _hubRepositoryClient.PersistChanges($"Branch {branch.Name} deleted for project {project.Name}"); - - } - - public async Task> ListKnowledgeLibraries() - { - var knowledgeLibraries = new List(); - var kbPath = Path.Combine(_hubRepositoryClient.Path, "kb"); - foreach (var knowledgelibraryFile in Directory.GetFiles(kbPath)) - { - try - { - var knowledgeLibrary = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(knowledgelibraryFile)); - if (knowledgeLibrary != null) - { - knowledgeLibraries.Add(knowledgeLibrary); - } - else - { - _logger.LogWarning($"Could not parse properties file for knowledgelibrary {knowledgelibraryFile}"); - } - } - catch(Exception ex) - { - _logger.LogError(ex, "Error with deserialization of file"); - } - } - return knowledgeLibraries; - } - internal async Task DeleteProject(string projectId) - { - var project = await GetProject(projectId); - - if (project == null) - { - return; - } - - if (_authentication.Enabled) - { - try - { - await _oidcClient.DeleteGroupAsync(project.Oidc?.GroupId ?? ""); - } catch (Exception ex) - { - _logger.LogError(ex, "Could not delete oidc group"); - } - } - - if (_git.Enabled) - { - try - { - await _repositoryApi.DeleteRepositoryAsync(project.Git?.Id ?? ""); - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not delete git repository"); - } - } - - if (_s3.Enabled) - { - try - { - await _s3Client.DeleteBucketAsync(project.S3?.BucketName ?? ""); - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not delete s3 bucket"); - } - } - - var branchPath = Path.Combine(_hubRepositoryClient.Path, "hub", projectId); - - _hubRepositoryClient.PullChanges(); - - if (Directory.Exists(branchPath)) - { - Directory.Delete(branchPath, true); - } - - _hubRepositoryClient.PersistChanges($"Project {project.Name} deleted"); - } - } -} diff --git a/src/Balsam/src/Balsam.Api/KnowledgeLibraryClient.cs b/src/Balsam/src/Balsam.Api/KnowledgeLibraryClient.cs deleted file mode 100644 index 45b4775..0000000 --- a/src/Balsam/src/Balsam.Api/KnowledgeLibraryClient.cs +++ /dev/null @@ -1,175 +0,0 @@ -using BalsamApi.Server.Models; -using LibGit2Sharp; -using System.IO.Compression; -using System.Text; -using static System.Net.Mime.MediaTypeNames; - -namespace Balsam.Api -{ - public class KnowledgeLibraryClient - { - public List GetRepositoryContent(string repositoryId, string repositoryUrl) - { - string localRepositoryPath = Path.Combine(Path.GetTempPath(), "kb", repositoryId); - - Repository repository = GetOrCreateRepository(localRepositoryPath, repositoryUrl); - - var repositoryContents = new List(); - - AddContents(localRepositoryPath, localRepositoryPath.Length + 1, repositoryContents); - - return repositoryContents; - } - - public string GetRepositoryFilePath(string repositoryId, string fileId) - { - string relativePath = Encoding.UTF8.GetString(Convert.FromBase64String(fileId)); - - if (relativePath.Contains("..")) - { - throw new ArgumentException("Invalid file id"); - } - - string filePath = Path.Combine(Path.GetTempPath(), "kb", repositoryId, relativePath); - - if (!File.Exists(filePath)) - { - throw new ArgumentException("File not found"); - } - - return filePath; - - } - - public string GetZippedResource(string repositoryId, string repositoryUrl, string fileId) - { - - //Make sure that the repository is cloned and up to date - string localRepositoryPath = Path.Combine(Path.GetTempPath(), "kb", repositoryId); - Repository repository = GetOrCreateRepository(localRepositoryPath, repositoryUrl); - - //Make sure that the file/directory exists in the repository - string relativePath = Encoding.UTF8.GetString(Convert.FromBase64String(fileId)); - if (relativePath.Contains("..")) - { - throw new ArgumentException("Invalid file id"); - } - - string filePath = Path.Combine(Path.GetTempPath(), "kb", repositoryId, relativePath); - - string workPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - - Directory.CreateDirectory(workPath); - - - if (File.Exists(filePath)) - { - File.Copy(filePath, Path.Combine(workPath, Path.GetFileName(filePath))); - } else if (Directory.Exists(filePath)) - { - CopyDirectory(filePath, workPath); - } - else - { - throw new ArgumentException("File/Directory not found"); - } - - string zipPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".zip"); - ZipFile.CreateFromDirectory(workPath, zipPath); - - Directory.Delete(workPath, true); - - return zipPath; - - } - - private static void CopyDirectory(string sourceDirName, string destDirName) - { - DirectoryInfo dir = new DirectoryInfo(sourceDirName); - - if (!dir.Exists) - { - throw new DirectoryNotFoundException( - "Source directory does not exist or could not be found: " - + sourceDirName); - } - - DirectoryInfo[] dirs = dir.GetDirectories(); - - // Append the name of the source directory to the destination directory - destDirName = Path.Combine(destDirName, dir.Name); - - // If the destination directory doesn't exist, create it. - Directory.CreateDirectory(destDirName); - - FileInfo[] files = dir.GetFiles(); - foreach (FileInfo file in files) - { - string tempPath = Path.Combine(destDirName, file.Name); - file.CopyTo(tempPath, false); - } - - foreach (DirectoryInfo subdir in dirs) - { - string tempPath = Path.Combine(destDirName, subdir.Name); - CopyDirectory(subdir.FullName, tempPath); - } - } - - - private Repository GetOrCreateRepository(string localRepositoryPath, string repositoryUrl) - { - if (Directory.Exists(localRepositoryPath)) - { - //Pull repository changes - var pullOptions = new PullOptions(); - pullOptions.FetchOptions = new FetchOptions(); - var repository = new Repository(localRepositoryPath); - Commands.Pull(repository, new Signature("x", "x@x.com", DateTimeOffset.Now), pullOptions); - return repository; - } - else - { - //Clone repository - Repository.Clone(repositoryUrl, localRepositoryPath); - return new Repository(localRepositoryPath); - } - } - - void AddContents(string path, int relativePathPosition, List contents) - { - foreach (var directory in Directory.GetDirectories(path)) - { - //Ignore .git folder - if (directory.Substring(relativePathPosition).StartsWith(".git") ) { continue; } - - contents.Add(CreateRepoFileFromDirectory(directory, relativePathPosition)); - AddContents(directory, relativePathPosition, contents); - } - foreach (var file in Directory.GetFiles(path)) - { - contents.Add(CreateRepoFileFromFile(file, relativePathPosition)); - } - } - - RepoFile CreateRepoFileFromFile(string path, int relativePathPosition) - { - var repoFile = new RepoFile(); - repoFile.Path = path.Substring(relativePathPosition); - repoFile.Name = Path.GetFileName(path); - repoFile.Id = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(repoFile.Path)); - repoFile.Type = RepoFile.TypeEnum.FileEnum; - return repoFile; - } - - RepoFile CreateRepoFileFromDirectory(string path, int relativePathPosition) - { - var repoFile = new RepoFile(); - repoFile.Path = path.Substring(relativePathPosition); - repoFile.Name = Path.GetFileName(path); - repoFile.Id = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(repoFile.Path)); - repoFile.Type = RepoFile.TypeEnum.FolderEnum; - return repoFile; - } - } -} diff --git a/src/Balsam/src/Balsam.Api/Models/WorkspaceContext.cs b/src/Balsam/src/Balsam.Api/Models/WorkspaceContext.cs deleted file mode 100644 index 501dad6..0000000 --- a/src/Balsam/src/Balsam.Api/Models/WorkspaceContext.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Balsam.Api.Models -{ - public class WorkspaceContext : BalsamContext - { - public BalsamProject Project { get; set; } - public BalsamBranch Branch { get; set; } - public BalsamWorkspace Workspace { get; set; } - public UserInfo User { get; set; } - - public WorkspaceContext(BalsamProject project, BalsamBranch branch, BalsamWorkspace workspace, UserInfo user) - { - Project = project; - Branch = branch; - Workspace = workspace; - User = user; - } - } -} diff --git a/src/Balsam/src/Balsam.Api/Program.cs b/src/Balsam/src/Balsam.Api/Program.cs index 8a89067..ef2ba11 100644 --- a/src/Balsam/src/Balsam.Api/Program.cs +++ b/src/Balsam/src/Balsam.Api/Program.cs @@ -1,8 +1,9 @@ using Balsam.Api; -using Balsam.Api.Models; +using Balsam.Interfaces; +using Balsam.Model; +using Balsam.Repositories; +using Balsam.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.HttpLogging; -using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); @@ -16,9 +17,17 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddSingleton(); -builder.Services.AddTransient(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); +builder.Services.AddTransient(); //TODO: Use AddScoped ? +builder.Services.AddTransient(); //TODO: Use AddScoped ? +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); builder.Services.AddMemoryCache(); builder.Services.AddSingleton( diff --git a/src/Balsam/src/Balsam.Api/README.md b/src/Balsam/src/Balsam.Api/README.md new file mode 100644 index 0000000..61b8d59 --- /dev/null +++ b/src/Balsam/src/Balsam.Api/README.md @@ -0,0 +1,3 @@ +#To build a local image with docker + + docker build -t balsamapi -f Dockerfile ..\ \ No newline at end of file diff --git a/src/Balsam/src/Balsam.Api/S3Client.cs b/src/Balsam/src/Balsam.Api/S3Client.cs deleted file mode 100644 index 5b93b4c..0000000 --- a/src/Balsam/src/Balsam.Api/S3Client.cs +++ /dev/null @@ -1,53 +0,0 @@ -//using Balsam.Api.Models; -//using Microsoft.Extensions.Options; -//using Newtonsoft.Json; -//using System.Net.Http.Headers; -//using System.Runtime.CompilerServices; -//using System.Security.Cryptography; -//using static System.Net.WebRequestMethods; - -//namespace Balsam.Api -//{ -// public class S3Client -// { -// private HttpClient _httpClient; - -// //private string _baseUrl = "http://s3-provider.balsam-system.svc.cluster.local/api/v1/buckets"; -// private string _baseUrl = "http://localhost:8080/api/v1"; -// public S3Client(IOptionsSnapshot capabilityOptions, HttpClient httpClient) { -// var s3Options = capabilityOptions.Get(Capabilities.S3); -// _baseUrl = s3Options.ServiceLocation; -// _httpClient = httpClient; - -// httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); -// httpClient.Timeout = TimeSpan.FromSeconds(30); - -// } - - -// public async Task CreateBucket(string bucketName) -// { -// HttpResponseMessage response = await _httpClient.PostAsync($"{_baseUrl}/buckets?preferredName={bucketName}", null); -// response.EnsureSuccessStatusCode(); - -// S3Data data = new S3Data(); - -// if (response.Content is object && response.Content.Headers.ContentType != null && response.Content.Headers.ContentType.MediaType == "application/json") -// { -// string content = await response.Content.ReadAsStringAsync(); -// if (content == null) -// { -// return null; -// } -// dynamic createResponse = JsonConvert.DeserializeObject(content); -// if (createResponse != null) -// { - - -// data.BucketName = createResponse.name; -// } -// } -// return data; -// } -// } -//} diff --git a/src/Balsam/src/Balsam.Application/Authorization/ProjectAuthorization.cs b/src/Balsam/src/Balsam.Application/Authorization/ProjectAuthorization.cs new file mode 100644 index 0000000..5aa0d10 --- /dev/null +++ b/src/Balsam/src/Balsam.Application/Authorization/ProjectAuthorization.cs @@ -0,0 +1,33 @@ +using Balsam.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; + +namespace Balsam.Application.Authorization +{ + public class ProjectAuthorization + { + public bool CanUserDeleteProject(ClaimsPrincipal user, BalsamProject project) + { + return user.Claims.Any(c => c.Type == "groups" && c.Value == project.Oidc.GroupName); + } + + public bool CanUserCreateBranch(ClaimsPrincipal user, BalsamProject project) + { + return user.Claims.Any(c => c.Type == "groups" && c.Value == project.Oidc.GroupName); + } + + public bool CanUserEditBranch(ClaimsPrincipal user, BalsamProject project) + { + return user.Claims.Any(c => c.Type == "groups" && c.Value == project.Oidc.GroupName); + } + + public bool CanUserDeleteBranch(ClaimsPrincipal user, BalsamProject project) + { + return user.Claims.Any(c => c.Type == "groups" && c.Value == project.Oidc.GroupName); + } + } +} diff --git a/src/Balsam/src/Balsam.Application/Authorization/WorkspaceAuthorization.cs b/src/Balsam/src/Balsam.Application/Authorization/WorkspaceAuthorization.cs new file mode 100644 index 0000000..182af0a --- /dev/null +++ b/src/Balsam/src/Balsam.Application/Authorization/WorkspaceAuthorization.cs @@ -0,0 +1,31 @@ +using Balsam.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; + +namespace Balsam.Application.Authorization +{ + public class WorkspaceAuthorization + { + public bool CanUserCreateWorkspace(ClaimsPrincipal user, BalsamProject project) + { + return user.Claims.Any(c => c.Type == "groups" && c.Value == project.Oidc.GroupName); + } + + public bool CanUserDeleteWorkspace(ClaimsPrincipal user, BalsamProject project, BalsamWorkspace workspace) + { + var userName = user.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value; + + return user.Claims.Any(c => c.Type == "groups" && c.Value == project.Oidc.GroupName) + && userName == workspace.Owner; + } + + public bool CanUserGetWorkspaces(ClaimsPrincipal user, BalsamProject project) + { + return user.Claims.Any(c => c.Type == "groups" && c.Value == project.Oidc.GroupName); + } + } +} diff --git a/src/Balsam/src/Balsam.Application/Balsam.Application.csproj b/src/Balsam/src/Balsam.Application/Balsam.Application.csproj new file mode 100644 index 0000000..0900a23 --- /dev/null +++ b/src/Balsam/src/Balsam.Application/Balsam.Application.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/src/Balsam/src/Balsam.Application/Extensions/RepoFileExtension.cs b/src/Balsam/src/Balsam.Application/Extensions/RepoFileExtension.cs new file mode 100644 index 0000000..4eeb2a5 --- /dev/null +++ b/src/Balsam/src/Balsam.Application/Extensions/RepoFileExtension.cs @@ -0,0 +1,34 @@ +using Balsam.Model; +using GitProviderApiClient.Model; + +namespace Balsam.Application.Extensions +{ + public static class RepoFileExtension + { + public static BalsamRepoFile ToBalsamRepoFile(this RepoFile repoFile) + { + var balsamRepoFile = new BalsamRepoFile + { + Id = repoFile.Id, + ContentUrl = repoFile.ContentUrl, + Name = repoFile.Name, + Path = repoFile.Path, + + }; + + switch (repoFile.Type) + { + case RepoFile.TypeEnum.File: + balsamRepoFile.Type = BalsamRepoFile.TypeEnum.FileEnum; + break; + case RepoFile.TypeEnum.Folder: + balsamRepoFile.Type = BalsamRepoFile.TypeEnum.FolderEnum; + break; + default: + throw new NotImplementedException(); + } + + return balsamRepoFile; + } + } +} diff --git a/src/Balsam/src/Balsam.Application/Interfaces/IKnowledgeLibraryService.cs b/src/Balsam/src/Balsam.Application/Interfaces/IKnowledgeLibraryService.cs new file mode 100644 index 0000000..e1cba5a --- /dev/null +++ b/src/Balsam/src/Balsam.Application/Interfaces/IKnowledgeLibraryService.cs @@ -0,0 +1,13 @@ +using Balsam.Model; + +namespace Balsam.Interfaces +{ + public interface IKnowledgeLibraryService + { + Task GetKnowledgeLibrary(string knowledgeLibraryId); + Task> GetRepositoryFileTree(string knowledgeLibraryId); + string GetRepositoryFilePath(string repositoryId, string fileId); + Task GetZippedResource(string repositoryId, string repositoryUrl, string fileId); + Task> ListKnowledgeLibraries(); + } +} \ No newline at end of file diff --git a/src/Balsam/src/Balsam.Application/Interfaces/IProjectService.cs b/src/Balsam/src/Balsam.Application/Interfaces/IProjectService.cs new file mode 100644 index 0000000..c1eafd9 --- /dev/null +++ b/src/Balsam/src/Balsam.Application/Interfaces/IProjectService.cs @@ -0,0 +1,20 @@ +using Balsam.Model; + +namespace Balsam.Interfaces +{ + public interface IProjectService + { + Task CopyFromKnowledgeLibrary(string projectId, string branchId, string knowledgeLibraryId, string fileId); + Task CreateBranch(string projectId, string fromBranch, string branchName, string description); + Task CreateProject(string preferredName, string description, string defaultBranchName, string username, string? sourceLocation = null); + Task DeleteBranch(string projectId, string branchId); + Task DeleteProject(string projectId); + Task GetBranch(string projectId, string branchId); + Task GetFile(string projectId, string branchId, string fileId); + Task?> GetGitBranchFiles(string projectId, string branchId); + Task GetProject(string projectId, bool includeBranches = true); + Task> GetProjects(bool includeBranches = true); + + Task ProjectExists(string preferredName); + } +} \ No newline at end of file diff --git a/src/Balsam/src/Balsam.Application/Interfaces/IWorkspaceService.cs b/src/Balsam/src/Balsam.Application/Interfaces/IWorkspaceService.cs new file mode 100644 index 0000000..8b80e09 --- /dev/null +++ b/src/Balsam/src/Balsam.Application/Interfaces/IWorkspaceService.cs @@ -0,0 +1,19 @@ + +using Balsam.Model; + +namespace Balsam.Interfaces +{ + public interface IWorkspaceService + { + Task CreateWorkspace(string projectId, string branchId, string name, string templateId, string userName, string userMail); + Task DeleteWorkspace(string projectId, string branchId, string workspaceId, string userName); + Task GetWorkspace(string projectId, string userId, string workspaceId); + Task> GetWorkspaces(); + Task> GetWorkspacesByProject(string projectId); + Task> GetWorkspacesByProjectAndBranch(string projectId, string branchId); + Task> GetWorkspacesByProjectAndUser(string projectId, string userId); + Task> GetWorkspacesByProjectBranchAndUser(string projectId, string branchId, string userId); + Task> GetWorkspacesByUser(string userId, List projectIds); + Task> ListWorkspaceTemplates(); + } +} \ No newline at end of file diff --git a/src/Balsam/src/Balsam.Application/Services/KnowledgeLibraryService.cs b/src/Balsam/src/Balsam.Application/Services/KnowledgeLibraryService.cs new file mode 100644 index 0000000..0999466 --- /dev/null +++ b/src/Balsam/src/Balsam.Application/Services/KnowledgeLibraryService.cs @@ -0,0 +1,43 @@ +using Balsam.Interfaces; +using Balsam.Model; +using Balsam.Repositories; + +namespace Balsam.Services +{ + + public class KnowledgeLibraryService : IKnowledgeLibraryService + { + private readonly IKnowledgeLibraryRepository _knowledgeLibraryRepository; + + + public KnowledgeLibraryService(IKnowledgeLibraryRepository knowledgeLibraryRepository) + { + _knowledgeLibraryRepository = knowledgeLibraryRepository; + } + + public async Task> ListKnowledgeLibraries() + { + return await _knowledgeLibraryRepository.ListKnowledgeLibraries(); + } + + public async Task GetKnowledgeLibrary(string knowledgeLibraryId) + { + return await _knowledgeLibraryRepository.GetKnowledgeLibrary(knowledgeLibraryId); + } + + public async Task> GetRepositoryFileTree(string knowledgeLibraryId) + { + return await _knowledgeLibraryRepository.GetRepositoryFileTree(knowledgeLibraryId); + } + + public string GetRepositoryFilePath(string repositoryId, string fileId) + { + return _knowledgeLibraryRepository.GetRepositoryFilePath(repositoryId, fileId); + } + + public async Task GetZippedResource(string repositoryId, string repositoryUrl, string fileId) + { + return await _knowledgeLibraryRepository.GetZippedResource(repositoryId, repositoryUrl, fileId); + } + } +} diff --git a/src/Balsam/src/Balsam.Application/Services/ProjectService.cs b/src/Balsam/src/Balsam.Application/Services/ProjectService.cs new file mode 100644 index 0000000..ab4b5c4 --- /dev/null +++ b/src/Balsam/src/Balsam.Application/Services/ProjectService.cs @@ -0,0 +1,352 @@ +using Balsam.Repositories; +using Balsam.Model; +using Microsoft.Extensions.Logging; +using S3ProviderApiClient.Api; //TODO: Split up generation of model and implementation and reference model. +using OidcProviderApiClient.Api; +using GitProviderApiClient.Api; +using RocketChatChatProviderApiClient.Api; +using Balsam.Interfaces; +using GitProviderApiClient.Model; +using Microsoft.Extensions.Options; +using OidcProviderApiClient.Model; +using S3ProviderApiClient.Model; +using System.ComponentModel.DataAnnotations; +using System.IO.Compression; +using System.Runtime.InteropServices; +using Balsam.Application.Extensions; + +namespace Balsam.Services +{ + public class ProjectService : IProjectService + { + private readonly CapabilityOptions _git; + private readonly CapabilityOptions _s3; + private readonly CapabilityOptions _authentication; + private readonly CapabilityOptions _chat; + private readonly IBucketApi _s3Client; + private readonly IGroupApi _oidcClient; //TODO: Rename IGroupAPI + private readonly IRepositoryApi _repositoryApi; + private readonly ILogger _logger; + private readonly IAreaApi _chatClient; //TODO: Rename IAreaApi + private readonly IProjectRepository _projectRepository; + private readonly IProjectGitOpsRepository _projectGitOpsRepository; + private readonly IKnowledgeLibraryService _knowledgeLibraryService; + + public ProjectService(ILogger logger, + IOptionsSnapshot capabilityOptions, + IBucketApi s3Client, + IRepositoryApi reposiotryApi, + IGroupApi oidcClient, + IAreaApi chatClient, + IProjectRepository projectRepository, + IProjectGitOpsRepository projectGitOpsRepository, + IKnowledgeLibraryService knowledgeLibraryService) + { + _logger = logger; + _s3Client = s3Client; + _oidcClient = oidcClient; + _chatClient = chatClient; + _projectRepository = projectRepository; + _projectGitOpsRepository = projectGitOpsRepository; + _knowledgeLibraryService = knowledgeLibraryService; + + _git = capabilityOptions.Get(Capabilities.Git); + _s3 = capabilityOptions.Get(Capabilities.S3); + _authentication = capabilityOptions.Get(Capabilities.Authentication); + _chat = capabilityOptions.Get(Capabilities.Chat); + _repositoryApi = reposiotryApi; + + } + + static ProjectService() + { + + } + + public async Task> GetProjects(bool includeBranches = true) + { + return await _projectRepository.GetProjects(includeBranches); + } + + public async Task GetProject(string projectId, bool includeBranches = true) + { + return await _projectRepository.GetProject(projectId, includeBranches); + } + + public async Task GetBranch(string projectId, string branchId) + { + return await _projectRepository.GetBranch(projectId, branchId); + } + + public async Task ProjectExists(string preferredName) + { + return await _projectRepository.ProjectExists(preferredName); + } + + public async Task CreateProject(string preferredName, string description, string defaultBranchName, string username, string? sourceLocation = null) + { + //Check if there is a program with the same name. + + _logger.LogDebug("Check for duplicate names"); + if (await _projectRepository.ProjectExists(preferredName)) + { + string errorMessage = $"Could not create project {preferredName}, due to name duplication"; + _logger.LogInformation(errorMessage); + throw new ApplicationException(errorMessage); + } + + _logger.LogDebug($"create project information"); + var project = new BalsamProject(preferredName, description); + + project = await _projectRepository.CreateProject(project, defaultBranchName); + + _logger.LogDebug($"Begin call service providers"); + + if (_authentication.Enabled) + { + _logger.LogDebug($"Begin call OpenIdConnect"); + var oidcData = await _oidcClient.CreateGroupAsync(new CreateGroupRequest(project.Id)); + await _oidcClient.AddUserToGroupAsync(oidcData.Id, new AddUserToGroupRequest(username)); + project.Oidc = new OidcData(oidcData.Id, oidcData.Name); + _logger.LogInformation($"Group {project.Oidc.GroupName}({project.Oidc.GroupId}) created"); + + if (project.Oidc == null) + { + throw new Exception("Could not parse oidc data"); + } + } + + if (_git.Enabled) + { + _logger.LogDebug($"Begin call Git"); + var gitData = await _repositoryApi.CreateRepositoryAsync(new CreateRepositoryRequest(preferredName, description, defaultBranchName)); + //defaultBranchName = gitData.DefaultBranchName; + project.Git = new GitData() { Id = gitData.Id, Name = gitData.Name, Path = gitData.Path, SourceLocation = sourceLocation }; + _logger.LogInformation($"Git repository {project.Git.Name} created"); + } + + if (_s3.Enabled) + { + _logger.LogDebug($"Begin call S3"); + var s3Data = await _s3Client.CreateBucketAsync(new CreateBucketRequest(preferredName, project.Oidc.GroupName)); + project.S3 = new S3Data() { BucketName = s3Data.Name }; + _logger.LogInformation($"Bucket {project.S3.BucketName} created"); + } + + if (_chat.Enabled) + { + _logger.LogDebug("Begin the call to chatprovider"); + var chatData = await _chatClient.CreateAreaAsync(new RocketChatChatProviderApiClient.Model.CreateAreaRequest(preferredName)); + project.Chat = new ChatData(chatData.Id, chatData.Name); + _logger.LogInformation($"Channel created named {chatData.Name}"); + } + + await _projectRepository.UpdateProject(project); + + await _projectGitOpsRepository.CreateProjectManifests(project); + + _logger.LogInformation($"Project {project.Name}({project.Id}) created"); + return project; + } + + public async Task CreateBranch(string projectId, string fromBranchId, string branchName, string description) + { + var project = await GetProject(projectId); + var fromBranch = await GetBranch(projectId, fromBranchId); + + if (fromBranch is null) + { + var errorMessage = $"Project with id {projectId} does not have a branch with id {fromBranchId}"; + _logger.LogError(errorMessage); + throw new ArgumentException(errorMessage, fromBranchId); + } + + if (project is null) + { + var errorMessage = $"Project with id {projectId} does not exist"; + _logger.LogError(errorMessage); + throw new ArgumentException(errorMessage, projectId); + } + + if (project.Git is null) + { + var errorMessage = $"Git-information not set for project with id {projectId}"; + _logger.LogError(errorMessage); + throw new ApplicationException(errorMessage); + } + + if (_git.Enabled) + { + var response = await _repositoryApi.CreateBranchAsync(project.Git.Id, new CreateBranchRequest(branchName, fromBranch.GitBranch)); + branchName = response.Name; + } + + return await _projectRepository.CreateBranch(projectId, branchName, description); + } + + public async Task?> GetGitBranchFiles(string projectId, string branchId) + { + var project = await GetProject(projectId); + + if(!_git.Enabled) + { + return new List(); + } + + if (project == null) + { + var errorMessage = $"Project with project id {projectId} does not exist"; + _logger.LogError(errorMessage); + throw new ArgumentException(errorMessage); + } + + var branch = await GetBranch(projectId, branchId); + + if (branch == null) + { + var errorMessage = $"Branch with branch id {branchId} in project {projectId} does not exist"; + _logger.LogError(errorMessage); + throw new ArgumentException(errorMessage); + } + + if (project.Git is null) + { + var errorMessage = $"Git definition missing for project {projectId}"; + _logger.LogError(errorMessage); + throw new ArgumentException(errorMessage); + } + + + return (await _repositoryApi.GetFilesInBranchAsync(project.Git.Id, branch.GitBranch)) + .Select(f => f.ToBalsamRepoFile()) + .ToList(); + } + + public async Task GetFile(string projectId, string branchId, string fileId) + { + //TODO: Use GitProvider instead... + return await _projectRepository.GetFile(projectId, branchId, fileId); + } + + public async Task DeleteBranch(string projectId, string branchId) + { + //var branchPath = Path.Combine(_hubRepositoryClient.Path, "hub", projectId, branchId); + _logger.LogDebug($"Deleting branch {branchId} for project {projectId}"); + + + if (_git.Enabled) + { + var project = await GetProject(projectId); + var branch = await GetBranch(projectId, branchId); + + //Asure that the id are correct + if (project == null || branch == null) return; + + try + { + _logger.LogDebug($"Deleting branch in git"); + await _repositoryApi.DeleteRepositoryBranchAsync(project.Git?.Id ?? "", branch.GitBranch); + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not delete git branch"); + } + } + + _logger.LogDebug($"Deleting branch information"); + await _projectRepository.DeleteBranch(projectId, branchId); + _logger.LogDebug($"Deleting branch manifests"); + + await _projectGitOpsRepository.DeleteBranchManifests(projectId, branchId); + + _logger.LogInformation($"Deleted branch {branchId} for project {projectId}"); + } + + public async Task DeleteProject(string projectId) + { + var project = await GetProject(projectId); + + if (project == null) + { + return; + } + + //TODO: Delete oidc group? + + if (_authentication.Enabled) + { + try + { + await _oidcClient.DeleteGroupAsync(project.Oidc?.GroupId ?? ""); + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not delete oidc group"); + } + } + + if (_git.Enabled) + { + try + { + await _repositoryApi.DeleteRepositoryAsync(project.Git?.Id ?? ""); + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not delete git repository"); + } + } + + if (_s3.Enabled) + { + try + { + await _s3Client.DeleteBucketAsync(project.S3?.BucketName ?? ""); + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not delete s3 bucket"); + } + } + + await _projectRepository.DeleteProject(projectId); + await _projectGitOpsRepository.DeleteProjectManifests(projectId); + + _logger.LogInformation($"Project {projectId} deleted"); + } + + public async Task CopyFromKnowledgeLibrary(string projectId, string branchId, string knowledgeLibraryId, string fileId) + { + var project = await GetProject(projectId); + + if (project is null) + { + var errorMessage = $"Project with id {projectId} does not exist"; + _logger.LogError(errorMessage); + throw new ArgumentException(errorMessage, projectId); + } + + if (project.Git is null) + { + var errorMessage = $"Git-information not set for project with id {projectId}"; + _logger.LogError(errorMessage); + throw new ApplicationException(errorMessage); + } + + var branch = project.Branches.First(b => b.Id == branchId); + var knowledgeLibrary = await _knowledgeLibraryService.GetKnowledgeLibrary(knowledgeLibraryId); + var zipFile = await _knowledgeLibraryService.GetZippedResource(knowledgeLibraryId, knowledgeLibrary.RepositoryUrl, fileId); + + try + { + using var stream = System.IO.File.OpenRead(zipFile); + await _repositoryApi.AddResourceFilesAsync(project.Git.Id, branch.GitBranch, stream); + } + finally + { + System.IO.File.Delete(zipFile); + } + + } + } +} diff --git a/src/Balsam/src/Balsam.Application/Services/WorkspaceService.cs b/src/Balsam/src/Balsam.Application/Services/WorkspaceService.cs new file mode 100644 index 0000000..58360c8 --- /dev/null +++ b/src/Balsam/src/Balsam.Application/Services/WorkspaceService.cs @@ -0,0 +1,176 @@ +using Balsam.Interfaces; +using Balsam.Model; +using Balsam.Repositories; +using GitProviderApiClient.Api; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using S3ProviderApiClient.Api; +using S3ProviderApiClient.Model; + +namespace Balsam.Services +{ + public class WorkspaceService : IWorkspaceService + { + private readonly CapabilityOptions _git; + private readonly CapabilityOptions _s3; + private readonly IWorkspaceRepository _workspaceRepository; + private readonly ILogger _logger; + private readonly IBucketApi _s3Client; + private readonly IUserApi _gitUserClient; + private readonly IProjectRepository _projectRepository; //TODO: User ProjectService instead? + private readonly IWorkspaceGitOpsRepository _workspaceGitOpsService; //TODO: Make and use interface + + public WorkspaceService(IOptionsSnapshot capabilityOptions, + IWorkspaceRepository workspaceRepository, + ILogger logger, + IBucketApi s3Client, + IWorkspaceGitOpsRepository workspaceGitOpsService, + IUserApi gitUserClient, + IProjectRepository projectRepository) + { + _git = capabilityOptions.Get(Capabilities.Git); + _s3 = capabilityOptions.Get(Capabilities.S3); + _workspaceRepository = workspaceRepository; + _logger = logger; + _s3Client = s3Client; + _workspaceGitOpsService = workspaceGitOpsService; + _gitUserClient = gitUserClient; + _projectRepository = projectRepository; + } + + + public async Task CreateWorkspace(string projectId, string branchId, string name, string templateId, string userName, string userMail) + { + var workspaceId = _workspaceRepository.GenerateWorkspaceId(name); + var workspace = new BalsamWorkspace(workspaceId, name, templateId, projectId, branchId, userName); + + await CreateWorkspaceManifests(projectId, branchId, templateId, userName, userMail, workspace); + + var template = await _workspaceRepository.GetWorkspaceTemplate(workspace.TemplateId); + + if (template != null) + { + workspace.Url = await _workspaceGitOpsService.GetWorkspaceUrl(projectId, branchId, userName, workspaceId, template.UrlConfig); + } + else + { + var errorMessage = $"Template with id {templateId} does not exist"; + _logger.LogError(errorMessage); + throw new ArgumentException(errorMessage); + } + + await _workspaceRepository.CreateWorkspace(workspace); + + _logger.LogInformation("Workspace created"); + return workspace; + } + + private async Task CreateWorkspaceManifests(string projectId, string branchId, string templateId, string userName, string userMail, BalsamWorkspace workspace) + { + var project = await _projectRepository.GetProject(projectId); + + if (project == null) + { + var errorMessage = $"Project with project id {projectId} does not exist"; + _logger.LogError(errorMessage); + throw new ArgumentException(errorMessage); + } + + var gitPAT = string.Empty; + + if (_git.Enabled) + { + var patResponse = await _gitUserClient.CreatePATAsync(userName); + gitPAT = patResponse.Token; + _logger.LogInformation($"Git PAT created"); + } + + var user = new UserInfo(userName, userMail, gitPAT); + + var token = new AccessKeyCreatedResponse("", ""); + if (_s3.Enabled && project.S3 != null) + { + token = await _s3Client.CreateAccessKeyAsync(project.S3.BucketName); + + var s3Token = new S3Token(token.AccessKey, token.SecretKey); + user.S3 = s3Token; + } + + _logger.LogDebug($"Copying manifests"); + + var branch = project.Branches.FirstOrDefault(b => b.Id == branchId); + + if (branch != null) + { + await _workspaceGitOpsService.CreateWorkspaceManifests(project, branch, workspace, user, templateId); + } + else + { + var errorMessage = $"Branch with branch id {branchId} in project {projectId} does not exist"; + _logger.LogError(errorMessage); + throw new ArgumentException(errorMessage); + } + + _logger.LogInformation("Manifests persisted"); + } + + public async Task DeleteWorkspace(string projectId, string branchId, string workspaceId, string userName) + { + await _workspaceRepository.DeleteWorkspace(projectId, branchId, workspaceId, userName); + + await _workspaceGitOpsService.DeleteWorkspaceManifests(projectId, branchId, workspaceId, userName); + + } + + public async Task> ListWorkspaceTemplates() + { + _logger.LogDebug("Start ListWorkspaceTemplates"); + + var workspaceTemplates = await _workspaceRepository.ListWorkspaceTemplates(); + + if (workspaceTemplates.Count() == 0) + { + _logger.LogInformation("No workspace template folder found!"); + + } + _logger.LogDebug("End ListWorkspaceTemplates"); + + return workspaceTemplates; + } + + public async Task> GetWorkspaces() + { + return await _workspaceRepository.GetWorkspaces(); + } + + public async Task> GetWorkspacesByUser(string userId, List projectIds) + { + return await _workspaceRepository.GetWorkspacesByUser(userId, projectIds); + } + + public async Task> GetWorkspacesByProject(string projectId) + { + return await _workspaceRepository.GetWorkspacesByProject(projectId); + } + + public async Task> GetWorkspacesByProjectAndBranch(string projectId, string branchId) + { + return await _workspaceRepository.GetWorkspacesByProjectAndBranch(projectId, branchId); + } + + public async Task> GetWorkspacesByProjectAndUser(string projectId, string userId) + { + return await _workspaceRepository.GetWorkspacesByProjectAndUser(projectId, userId); + } + + public async Task> GetWorkspacesByProjectBranchAndUser(string projectId, string branchId, string userId) + { + return await _workspaceRepository.GetWorkspacesByProjectBranchAndUser(projectId, branchId, userId); + } + + public async Task GetWorkspace(string projectId, string userId, string workspaceId) + { + return await _workspaceRepository.GetWorkspace(projectId, userId, workspaceId); + } + } +} diff --git a/src/Balsam/src/Balsam.Domain/Balsam.Domain.csproj b/src/Balsam/src/Balsam.Domain/Balsam.Domain.csproj new file mode 100644 index 0000000..bf34ca9 --- /dev/null +++ b/src/Balsam/src/Balsam.Domain/Balsam.Domain.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + + + + + + + + diff --git a/src/Balsam/src/Balsam.Api/Models/BalsamBranch.cs b/src/Balsam/src/Balsam.Domain/Model/BalsamBranch.cs similarity index 89% rename from src/Balsam/src/Balsam.Api/Models/BalsamBranch.cs rename to src/Balsam/src/Balsam.Domain/Model/BalsamBranch.cs index 0a936de..3b56b04 100644 --- a/src/Balsam/src/Balsam.Api/Models/BalsamBranch.cs +++ b/src/Balsam/src/Balsam.Domain/Model/BalsamBranch.cs @@ -1,4 +1,4 @@ -namespace Balsam.Api.Models +namespace Balsam.Model { public class BalsamBranch { diff --git a/src/Balsam/src/Balsam.Api/Models/BalsamContext.cs b/src/Balsam/src/Balsam.Domain/Model/BalsamContext.cs similarity index 60% rename from src/Balsam/src/Balsam.Api/Models/BalsamContext.cs rename to src/Balsam/src/Balsam.Domain/Model/BalsamContext.cs index 9971c29..273b5fb 100644 --- a/src/Balsam/src/Balsam.Api/Models/BalsamContext.cs +++ b/src/Balsam/src/Balsam.Domain/Model/BalsamContext.cs @@ -1,4 +1,4 @@ -namespace Balsam.Api.Models +namespace Balsam.Model { public class BalsamContext { diff --git a/src/Balsam/src/Balsam.Domain/Model/BalsamKnowledgeLibrary.cs b/src/Balsam/src/Balsam.Domain/Model/BalsamKnowledgeLibrary.cs new file mode 100644 index 0000000..d39f219 --- /dev/null +++ b/src/Balsam/src/Balsam.Domain/Model/BalsamKnowledgeLibrary.cs @@ -0,0 +1,15 @@ +namespace Balsam.Model +{ + public class BalsamKnowledgeLibrary + { + public string Id { get; set; } + + public string Name { get; set; } + + public string? Description { get; set; } + + public string RepositoryUrl { get; set; } + + public string? RepositoryFriendlyUrl { get; set; } + } +} diff --git a/src/Balsam/src/Balsam.Api/Models/BalsamProject.cs b/src/Balsam/src/Balsam.Domain/Model/BalsamProject.cs similarity index 81% rename from src/Balsam/src/Balsam.Api/Models/BalsamProject.cs rename to src/Balsam/src/Balsam.Domain/Model/BalsamProject.cs index fed35ee..acf6330 100644 --- a/src/Balsam/src/Balsam.Api/Models/BalsamProject.cs +++ b/src/Balsam/src/Balsam.Domain/Model/BalsamProject.cs @@ -1,4 +1,4 @@ -namespace Balsam.Api.Models +namespace Balsam.Model { public class BalsamProject { @@ -12,9 +12,8 @@ public class BalsamProject public List Branches { get; set; } - public BalsamProject(string id, string name, string description) + public BalsamProject(string name, string description) { - Id = id; Name = name; Description = description; Branches = new List(); diff --git a/src/Balsam/src/Balsam.Domain/Model/BalsamRepoFile.cs b/src/Balsam/src/Balsam.Domain/Model/BalsamRepoFile.cs new file mode 100644 index 0000000..c06783b --- /dev/null +++ b/src/Balsam/src/Balsam.Domain/Model/BalsamRepoFile.cs @@ -0,0 +1,34 @@ +using System.Runtime.Serialization; + +namespace Balsam.Model +{ + //TODO: Rename to RepoFileTreeNode or something + public class BalsamRepoFile + { + public string Id { get; set; } + + public string Path { get; set; } + + public string Name { get; set; } + + public enum TypeEnum + { + + /// + /// Enum FileEnum for File + /// + [EnumMember(Value = "File")] + FileEnum = 1, + + /// + /// Enum FolderEnum for Folder + /// + [EnumMember(Value = "Folder")] + FolderEnum = 2 + } + + public TypeEnum Type { get; set; } + + public string? ContentUrl { get; set; } + } +} diff --git a/src/Balsam/src/Balsam.Api/Models/BalsamWorkspace.cs b/src/Balsam/src/Balsam.Domain/Model/BalsamWorkspace.cs similarity index 95% rename from src/Balsam/src/Balsam.Api/Models/BalsamWorkspace.cs rename to src/Balsam/src/Balsam.Domain/Model/BalsamWorkspace.cs index 36817d9..adeed67 100644 --- a/src/Balsam/src/Balsam.Api/Models/BalsamWorkspace.cs +++ b/src/Balsam/src/Balsam.Domain/Model/BalsamWorkspace.cs @@ -1,4 +1,4 @@ -namespace Balsam.Api.Models +namespace Balsam.Model { public class BalsamWorkspace { diff --git a/src/Balsam/src/Balsam.Domain/Model/CapabilityOptions.cs b/src/Balsam/src/Balsam.Domain/Model/CapabilityOptions.cs new file mode 100644 index 0000000..e2f152b --- /dev/null +++ b/src/Balsam/src/Balsam.Domain/Model/CapabilityOptions.cs @@ -0,0 +1,17 @@ +namespace Balsam.Model +{ + public class CapabilityOptions + { + public bool Enabled { get; set; } + public string? ServiceLocation { get; set; } + public string[]? Actions { get; set; } + } + + public class Capabilities + { + public const string Git = "Git"; + public const string S3 = "S3"; + public const string Authentication = "Authentication"; + public const string Chat = "Chat"; + } +} diff --git a/src/Balsam/src/Balsam.Api/Models/ChatData.cs b/src/Balsam/src/Balsam.Domain/Model/ChatData.cs similarity index 88% rename from src/Balsam/src/Balsam.Api/Models/ChatData.cs rename to src/Balsam/src/Balsam.Domain/Model/ChatData.cs index 9a37875..a0cddf3 100644 --- a/src/Balsam/src/Balsam.Api/Models/ChatData.cs +++ b/src/Balsam/src/Balsam.Domain/Model/ChatData.cs @@ -1,4 +1,4 @@ -namespace Balsam.Api.Models +namespace Balsam.Model { public class ChatData { diff --git a/src/Balsam/src/Balsam.Domain/Model/FileContent.cs b/src/Balsam/src/Balsam.Domain/Model/FileContent.cs new file mode 100644 index 0000000..9a99e41 --- /dev/null +++ b/src/Balsam/src/Balsam.Domain/Model/FileContent.cs @@ -0,0 +1,20 @@ +namespace Balsam.Model +{ + public class FileContent + { + + public byte[]? Content { get; set; } + public string Mediatype { get; set; } = string.Empty; + + public FileContent(byte[]? content, string mediatype) + { + Content = content; + Mediatype = mediatype; + } + + public FileContent() + { + + } + } +} diff --git a/src/Balsam/src/Balsam.Api/Models/GitData.cs b/src/Balsam/src/Balsam.Domain/Model/GitData.cs similarity index 61% rename from src/Balsam/src/Balsam.Api/Models/GitData.cs rename to src/Balsam/src/Balsam.Domain/Model/GitData.cs index 7b9d71a..c639864 100644 --- a/src/Balsam/src/Balsam.Api/Models/GitData.cs +++ b/src/Balsam/src/Balsam.Domain/Model/GitData.cs @@ -1,13 +1,13 @@ -using System.Security.Principal; - -namespace Balsam.Api.Models -{ - public class GitData - { - public string Id { get; set; } - public string Name { get; set; } - public string Path { get; set; } - public string? SourceLocation { get; set; } - public bool HasTemplate { get { return !string.IsNullOrEmpty(SourceLocation); } } - } -} +using System.Security.Principal; + +namespace Balsam.Model +{ + public class GitData + { + public string Id { get; set; } + public string Name { get; set; } + public string Path { get; set; } + public string? SourceLocation { get; set; } //TODO: What is this used for? + public bool HasTemplate { get { return !string.IsNullOrEmpty(SourceLocation); } } //TODO: What is this used for? + } +} diff --git a/src/Balsam/src/Balsam.Domain/Model/HubPaths.cs b/src/Balsam/src/Balsam.Domain/Model/HubPaths.cs new file mode 100644 index 0000000..4b31539 --- /dev/null +++ b/src/Balsam/src/Balsam.Domain/Model/HubPaths.cs @@ -0,0 +1,67 @@ + +namespace Balsam.Model +{ + public class HubPaths + { + public string BasePath { get; } + + public HubPaths(string basePath) + { + BasePath = basePath; + } + + public string GetTemplatesPath() + { + return Path.Combine(BasePath, "templates"); + } + + public string GetHubPath() + { + return Path.Combine(BasePath, "hub"); + } + + public string GetProjectPath(string projectId) + { + return Path.Combine(GetHubPath(), projectId); + } + + public string GetBranchPath(string projectId, string branchId) + { + return Path.Combine(GetProjectPath(projectId), branchId); + } + public string GetUserPath(string projectId, string branchId, string userName) + { + return Path.Combine(GetBranchPath(projectId, branchId), userName); + } + + public string GetWorkspacePath(string projectId, string branchId, string userName, string workspaceId) + { + return Path.Combine(GetUserPath(projectId, branchId, userName), workspaceId); + } + + public string GetWorkspaceTemplatesPath(string templateId) + { + return Path.Combine(GetWorkspacesTemplatesPath(), templateId); + } + + public string GetProjectsTemplatesPath() + { + return Path.Combine(GetTemplatesPath(), "projects"); + } + + public string GetWorkspacesTemplatesPath() + { + return Path.Combine(GetTemplatesPath(), "workspaces"); + } + + public string GetKnowledgeLibrariesPath() + { + return Path.Combine(BasePath, "kb"); + } + + public string GetKnowledgeLibraryFilePath(string knowledgeLibraryFileId) + { + return Path.Combine(GetKnowledgeLibrariesPath(), $"{knowledgeLibraryFileId}.json"); + } + } +} diff --git a/src/Balsam/src/Balsam.Api/Models/HubRepositoryOptions.cs b/src/Balsam/src/Balsam.Domain/Model/HubRepositoryOptions.cs similarity index 90% rename from src/Balsam/src/Balsam.Api/Models/HubRepositoryOptions.cs rename to src/Balsam/src/Balsam.Domain/Model/HubRepositoryOptions.cs index 56a9467..d41552d 100644 --- a/src/Balsam/src/Balsam.Api/Models/HubRepositoryOptions.cs +++ b/src/Balsam/src/Balsam.Domain/Model/HubRepositoryOptions.cs @@ -1,4 +1,4 @@ -namespace Balsam.Api.Models +namespace Balsam.Model { public class HubRepositoryOptions { diff --git a/src/Balsam/src/Balsam.Api/Models/OidcData.cs b/src/Balsam/src/Balsam.Domain/Model/OidcData.cs similarity index 89% rename from src/Balsam/src/Balsam.Api/Models/OidcData.cs rename to src/Balsam/src/Balsam.Domain/Model/OidcData.cs index 2f0ff78..890d799 100644 --- a/src/Balsam/src/Balsam.Api/Models/OidcData.cs +++ b/src/Balsam/src/Balsam.Domain/Model/OidcData.cs @@ -1,4 +1,4 @@ -namespace Balsam.Api.Models +namespace Balsam.Model { public class OidcData { diff --git a/src/Balsam/src/Balsam.Api/Models/ProjectContext.cs b/src/Balsam/src/Balsam.Domain/Model/ProjectContext.cs similarity index 78% rename from src/Balsam/src/Balsam.Api/Models/ProjectContext.cs rename to src/Balsam/src/Balsam.Domain/Model/ProjectContext.cs index f978b2f..8f843b1 100644 --- a/src/Balsam/src/Balsam.Api/Models/ProjectContext.cs +++ b/src/Balsam/src/Balsam.Domain/Model/ProjectContext.cs @@ -1,4 +1,4 @@ -namespace Balsam.Api.Models +namespace Balsam.Model { public class ProjectContext : BalsamContext { diff --git a/src/Balsam/src/Balsam.Api/Models/S3Data.cs b/src/Balsam/src/Balsam.Domain/Model/S3Data.cs similarity index 73% rename from src/Balsam/src/Balsam.Api/Models/S3Data.cs rename to src/Balsam/src/Balsam.Domain/Model/S3Data.cs index bcb86ab..f7bc95e 100644 --- a/src/Balsam/src/Balsam.Api/Models/S3Data.cs +++ b/src/Balsam/src/Balsam.Domain/Model/S3Data.cs @@ -1,4 +1,4 @@ -namespace Balsam.Api.Models +namespace Balsam.Model { public class S3Data { diff --git a/src/Balsam/src/Balsam.Api/Models/S3Token.cs b/src/Balsam/src/Balsam.Domain/Model/S3Token.cs similarity index 90% rename from src/Balsam/src/Balsam.Api/Models/S3Token.cs rename to src/Balsam/src/Balsam.Domain/Model/S3Token.cs index 9e90fc7..1935731 100644 --- a/src/Balsam/src/Balsam.Api/Models/S3Token.cs +++ b/src/Balsam/src/Balsam.Domain/Model/S3Token.cs @@ -1,4 +1,4 @@ -namespace Balsam.Api.Models +namespace Balsam.Model { public class S3Token { diff --git a/src/Balsam/src/Balsam.Api/Models/UserInfo.cs b/src/Balsam/src/Balsam.Domain/Model/UserInfo.cs similarity index 92% rename from src/Balsam/src/Balsam.Api/Models/UserInfo.cs rename to src/Balsam/src/Balsam.Domain/Model/UserInfo.cs index f6a0958..8c1760c 100644 --- a/src/Balsam/src/Balsam.Api/Models/UserInfo.cs +++ b/src/Balsam/src/Balsam.Domain/Model/UserInfo.cs @@ -1,4 +1,4 @@ -namespace Balsam.Api.Models +namespace Balsam.Model { public class UserInfo { diff --git a/src/Balsam/src/Balsam.Domain/Model/WorkspaceContext.cs b/src/Balsam/src/Balsam.Domain/Model/WorkspaceContext.cs new file mode 100644 index 0000000..2cf4633 --- /dev/null +++ b/src/Balsam/src/Balsam.Domain/Model/WorkspaceContext.cs @@ -0,0 +1,17 @@ +namespace Balsam.Model; + +public class WorkspaceContext : BalsamContext +{ + public BalsamProject Project { get; set; } + public BalsamBranch Branch { get; set; } + public BalsamWorkspace Workspace { get; set; } + public UserInfo User { get; set; } + + public WorkspaceContext(BalsamProject project, BalsamBranch branch, BalsamWorkspace workspace, UserInfo user) + { + Project = project; + Branch = branch; + Workspace = workspace; + User = user; + } +} diff --git a/src/Balsam/src/Balsam.Api/Models/WorkspaceTemplate.cs b/src/Balsam/src/Balsam.Domain/Model/WorkspaceTemplate.cs similarity index 85% rename from src/Balsam/src/Balsam.Api/Models/WorkspaceTemplate.cs rename to src/Balsam/src/Balsam.Domain/Model/WorkspaceTemplate.cs index a514510..16dee59 100644 --- a/src/Balsam/src/Balsam.Api/Models/WorkspaceTemplate.cs +++ b/src/Balsam/src/Balsam.Domain/Model/WorkspaceTemplate.cs @@ -1,4 +1,4 @@ -namespace Balsam.Api.Models; +namespace Balsam.Model; public class WorkspaceTemplate { diff --git a/src/Balsam/src/Balsam.Domain/Repository/IHubRepositoryClient.cs b/src/Balsam/src/Balsam.Domain/Repository/IHubRepositoryClient.cs new file mode 100644 index 0000000..e7a7c13 --- /dev/null +++ b/src/Balsam/src/Balsam.Domain/Repository/IHubRepositoryClient.cs @@ -0,0 +1,9 @@ +namespace Balsam.Repositories +{ + public interface IHubRepositoryClient + { + string Path { get; } + void PersistChanges(string commitMessage); + void PullChanges(); + } +} \ No newline at end of file diff --git a/src/Balsam/src/Balsam.Domain/Repository/IKnowledgeLibraryContentRepository.cs b/src/Balsam/src/Balsam.Domain/Repository/IKnowledgeLibraryContentRepository.cs new file mode 100644 index 0000000..28a378c --- /dev/null +++ b/src/Balsam/src/Balsam.Domain/Repository/IKnowledgeLibraryContentRepository.cs @@ -0,0 +1,11 @@ +using Balsam.Model; + +namespace Balsam.Repositories +{ + public interface IKnowledgeLibraryContentRepository + { + Task> GetFileTree(string repositoryId, string repositoryUrl); + string GetFilePath(string repositoryId, string fileId); + Task GetZippedResource(string repositoryId, string repositoryUrl, string fileId); + } +} \ No newline at end of file diff --git a/src/Balsam/src/Balsam.Domain/Repository/IKnowledgeLibraryRepository.cs b/src/Balsam/src/Balsam.Domain/Repository/IKnowledgeLibraryRepository.cs new file mode 100644 index 0000000..a2be31b --- /dev/null +++ b/src/Balsam/src/Balsam.Domain/Repository/IKnowledgeLibraryRepository.cs @@ -0,0 +1,13 @@ +using Balsam.Model; + +namespace Balsam.Repositories +{ + public interface IKnowledgeLibraryRepository + { + Task GetKnowledgeLibrary(string knowledgeLibraryId); + Task> GetRepositoryFileTree(string knowledgeLibraryId); + string GetRepositoryFilePath(string repositoryId, string fileId); + Task GetZippedResource(string repositoryId, string repositoryUrl, string fileId); + Task> ListKnowledgeLibraries(); + } +} \ No newline at end of file diff --git a/src/Balsam/src/Balsam.Domain/Repository/IProjectGitOpsRepository.cs b/src/Balsam/src/Balsam.Domain/Repository/IProjectGitOpsRepository.cs new file mode 100644 index 0000000..8fcab3b --- /dev/null +++ b/src/Balsam/src/Balsam.Domain/Repository/IProjectGitOpsRepository.cs @@ -0,0 +1,11 @@ +using Balsam.Model; + +namespace Balsam.Repositories +{ + public interface IProjectGitOpsRepository + { + Task CreateProjectManifests(BalsamProject project); + Task DeleteBranchManifests(string projectId, string branchId); + Task DeleteProjectManifests(string projectId); + } +} \ No newline at end of file diff --git a/src/Balsam/src/Balsam.Domain/Repository/IProjectRepository.cs b/src/Balsam/src/Balsam.Domain/Repository/IProjectRepository.cs new file mode 100644 index 0000000..73ac35d --- /dev/null +++ b/src/Balsam/src/Balsam.Domain/Repository/IProjectRepository.cs @@ -0,0 +1,21 @@ + +using Balsam.Model; + +namespace Balsam.Repositories +{ + public interface IProjectRepository + { + Task CreateBalsamBranchFromGitBranch(string projectId, string gitBranchName); + Task CreateBranch(string projectId, string branchName, string description); + Task CreateProject(BalsamProject project, string defaultBranchName); + Task DeleteBranch(string projectId, string branchId); + Task DeleteProject(string projectId); + Task GetBranch(string projectId, string branchId); + //Task GetProject(string projectId); + Task GetProject(string projectId, bool includeBranches = true); + Task> GetProjects(bool includeBranches = true); + Task ProjectExists(string projectName); + Task GetFile(string projectId, string branchId, string fileId); + Task UpdateProject(BalsamProject project); + } +} \ No newline at end of file diff --git a/src/Balsam/src/Balsam.Domain/Repository/IWorkspaceGitOpsRepository.cs b/src/Balsam/src/Balsam.Domain/Repository/IWorkspaceGitOpsRepository.cs new file mode 100644 index 0000000..986623e --- /dev/null +++ b/src/Balsam/src/Balsam.Domain/Repository/IWorkspaceGitOpsRepository.cs @@ -0,0 +1,12 @@ + +using Balsam.Model; + +namespace Balsam.Repositories +{ + public interface IWorkspaceGitOpsRepository + { + Task CreateWorkspaceManifests(BalsamProject project, BalsamBranch branch, BalsamWorkspace workspace, UserInfo user, string templateId); + Task DeleteWorkspaceManifests(string projectId, string branchId, string workspaceId, string userName); + Task GetWorkspaceUrl(string projectId, string branchId, string userName, string workspaceId, string urlConfigFile); + } +} \ No newline at end of file diff --git a/src/Balsam/src/Balsam.Domain/Repository/IWorkspaceRepository.cs b/src/Balsam/src/Balsam.Domain/Repository/IWorkspaceRepository.cs new file mode 100644 index 0000000..c1081e4 --- /dev/null +++ b/src/Balsam/src/Balsam.Domain/Repository/IWorkspaceRepository.cs @@ -0,0 +1,21 @@ + +using Balsam.Model; + +namespace Balsam.Repositories +{ + public interface IWorkspaceRepository + { + Task CreateWorkspace(BalsamWorkspace workspace); + Task DeleteWorkspace(string projectId, string branchId, string workspaceId, string userName); + string GenerateWorkspaceId(string name); + Task GetWorkspace(string projectId, string userId, string workspaceId); + Task> GetWorkspaces(); + Task> GetWorkspacesByProject(string projectId); + Task> GetWorkspacesByProjectAndBranch(string projectId, string branchId); + Task> GetWorkspacesByProjectAndUser(string projectId, string userId); + Task> GetWorkspacesByProjectBranchAndUser(string projectId, string branchId, string userId); + Task> GetWorkspacesByUser(string userId, List projectIds); + Task GetWorkspaceTemplate(string templateId); + Task> ListWorkspaceTemplates(); + } +} \ No newline at end of file diff --git a/src/Balsam/src/Balsam.Domain/Utility/NameUtil.cs b/src/Balsam/src/Balsam.Domain/Utility/NameUtil.cs new file mode 100644 index 0000000..3a3fd73 --- /dev/null +++ b/src/Balsam/src/Balsam.Domain/Utility/NameUtil.cs @@ -0,0 +1,25 @@ +using System.Text.RegularExpressions; + +namespace Balsam.Utility +{ + public static class NameUtil + { + public static string SanitizeName(string name) + { + //TODO: Combine with GenerateWorkspaceId + var crc32 = new System.IO.Hashing.Crc32(); + + crc32.Append(System.Text.Encoding.ASCII.GetBytes(name)); + var hash = crc32.GetCurrentHash(); + var crcHash = string.Join("", hash.Select(b => b.ToString("x2").ToLower()).Reverse()); + + name = name.ToLower(); //Only lower charachters allowed + name = name.Replace(" ", "-"); //replaces spaches with hypen + name = Regex.Replace(name, @"[^a-z0-9\-]", ""); // make sure that only a-z or digit or hypen removes all other characters + name = name.Substring(0, Math.Min(50 - crcHash.Length, name.Length)) + "-" + crcHash; //Assures max size of 50 characters + + return name; + + } + } +} diff --git a/src/Balsam/src/Balsam.Infrastructure/Balsam.Infrastructure.csproj b/src/Balsam/src/Balsam.Infrastructure/Balsam.Infrastructure.csproj new file mode 100644 index 0000000..3d9c28e --- /dev/null +++ b/src/Balsam/src/Balsam.Infrastructure/Balsam.Infrastructure.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/src/Balsam/src/Balsam.Infrastructure/Repositories/GitOpsRepository.cs b/src/Balsam/src/Balsam.Infrastructure/Repositories/GitOpsRepository.cs new file mode 100644 index 0000000..a491a63 --- /dev/null +++ b/src/Balsam/src/Balsam.Infrastructure/Repositories/GitOpsRepository.cs @@ -0,0 +1,69 @@ +using Balsam.Model; +using Balsam.Utility; +using HandlebarsDotNet; + +namespace Balsam.Repositories +{ + public class GitOpsRepository + { + protected readonly IHubRepositoryClient HubRepositoryClient; + protected HubPaths HubPaths { get; set; } + + + public GitOpsRepository(IHubRepositoryClient hubRepositoryClient) + { + HubRepositoryClient = hubRepositoryClient; + HubPaths = new HubPaths(HubRepositoryClient.Path); + + Handlebars.RegisterHelper("curlies", (writer, context, parameters) => + { + if (parameters.Length == 1 && parameters.At(0) == true) + { + writer.Write("{{"); + } + else + { + writer.Write("}}"); + } + + }); + } + + /// + /// Copies manifest-templates and replaces templated variables like {{variable}} with context value + /// + /// The Context for the template + /// The destination path for the manifest + /// The path to the template relative to the path of all templates + /// + protected async Task CreateManifests(BalsamContext context, string destinationPath, string templatePath) + { + + //var templatesPath = HubPaths.GetTemplatesPath(); + //var templateFullPath = Path.Combine(templatesPath, templateRelativePath); + + DirectoryUtil.AssureDirectoryExists(destinationPath); + + foreach (var file in Directory.GetFiles(templatePath, "*.yaml")) + { + var source = await File.ReadAllTextAsync(file); + + var template = Handlebars.Compile(source); + + var result = template(context); + + var destinationFilePath = Path.Combine(destinationPath, Path.GetFileName(file)); + + await File.WriteAllTextAsync(destinationFilePath, result); + } + } + + protected async Task DeleteManifests(string path) + { + if (Directory.Exists(path)) + { + await Task.Run(() => Directory.Delete(path, true)); + } + } + } +} diff --git a/src/Balsam/src/Balsam.Api/HubRepositoryClient.cs b/src/Balsam/src/Balsam.Infrastructure/Repositories/HubRepositoryClient.cs similarity index 73% rename from src/Balsam/src/Balsam.Api/HubRepositoryClient.cs rename to src/Balsam/src/Balsam.Infrastructure/Repositories/HubRepositoryClient.cs index 777a3c6..69e15ed 100644 --- a/src/Balsam/src/Balsam.Api/HubRepositoryClient.cs +++ b/src/Balsam/src/Balsam.Infrastructure/Repositories/HubRepositoryClient.cs @@ -1,10 +1,12 @@ -using Balsam.Api.Models; +using Balsam.Model; +using Balsam.Utility; using LibGit2Sharp; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Balsam.Api +namespace Balsam.Repositories { - public class HubRepositoryClient + public class HubRepositoryClient : IHubRepositoryClient { private readonly string _repositoryPath; private readonly Repository _repository; @@ -15,15 +17,17 @@ public class HubRepositoryClient public string Path { get { return _repositoryPath; } } - public HubRepositoryClient(IOptions options, ILogger logger) { + public HubRepositoryClient(IOptions options, ILogger logger) + { _logger = logger; _userId = options.Value.User; _password = options.Value.Password; - _mail = options.Value.Mail??"noreply@balsam.local"; + _mail = options.Value.Mail ?? "noreply@balsam.local"; //Assure credentials - if (string.IsNullOrEmpty(_userId) || string.IsNullOrEmpty(_password)) { + if (string.IsNullOrEmpty(_userId) || string.IsNullOrEmpty(_password)) + { _logger.LogWarning("Missing hub repository credentials"); } @@ -37,17 +41,22 @@ public HubRepositoryClient(IOptions options, ILogger new UsernamePasswordCredentials { Username = _userId, Password = _password } + //} CredentialsProvider = (_url, _user, _cred) => new UsernamePasswordCredentials { Username = _userId, Password = _password } }; _logger.LogInformation("Begin clone Hub repo"); + _repositoryPath = Repository.Clone(options.Value.RemoteUrl, basePath, cloneOptions); _logger.LogInformation("End clone Hub repo"); _repository = new Repository(_repositoryPath); _repositoryPath = basePath; } - + public void PersistChanges(string commitMessage) { var pushOptions = new PushOptions(); @@ -58,11 +67,15 @@ public void PersistChanges(string commitMessage) Commands.Stage(_repository, "*"); - _repository.Commit(commitMessage, namesig, namesig); + RepositoryStatus status = _repository.RetrieveStatus(); + + if (status.IsDirty) { + _repository.Commit(commitMessage, namesig, namesig); - var remote = _repository.Network.Remotes["origin"]; + var remote = _repository.Network.Remotes["origin"]; - _repository.Network.Push(remote, _repository.Head.CanonicalName, pushOptions); + _repository.Network.Push(remote, _repository.Head.CanonicalName, pushOptions); + } } public void PullChanges() diff --git a/src/Balsam/src/Balsam.Infrastructure/Repositories/KnowledgeLibraryContentRepository.cs b/src/Balsam/src/Balsam.Infrastructure/Repositories/KnowledgeLibraryContentRepository.cs new file mode 100644 index 0000000..373e9ea --- /dev/null +++ b/src/Balsam/src/Balsam.Infrastructure/Repositories/KnowledgeLibraryContentRepository.cs @@ -0,0 +1,196 @@ +using Balsam.Model; +using LibGit2Sharp; +using System.IO.Compression; +using System.Text; + +namespace Balsam.Repositories +{ + public class KnowledgeLibraryContentRepository : IKnowledgeLibraryContentRepository + { + public async Task> GetFileTree(string repositoryId, string repositoryUrl) + { + string localRepositoryPath = GetLocalRepositoryPath(repositoryId); + + await CloneOrPull(localRepositoryPath, repositoryUrl); + + var repositoryContents = new List(); + + await GetFileTree(localRepositoryPath, localRepositoryPath.Length + 1, repositoryContents); + + return repositoryContents; + } + + private static string GetLocalRepositoryPath(string repositoryId) + { + return Path.Combine(Path.GetTempPath(), "kb", repositoryId); + } + + public string GetFilePath(string repositoryId, string fileId) + { + string relativePath = Encoding.UTF8.GetString(Convert.FromBase64String(fileId)); + + if (relativePath.Contains("..")) + { + throw new ArgumentException("Invalid file id"); + } + + string localRepositoryPath = GetLocalRepositoryPath(repositoryId); + + string filePath = Path.Combine(localRepositoryPath, relativePath); + + if (!File.Exists(filePath)) + { + throw new ArgumentException("File not found"); + } + + return filePath; + + } + + /// + /// Zips a resoruce (file or directory) and returns a path to the ziped file + /// + public async Task GetZippedResource(string repositoryId, string repositoryUrl, string fileId) + { + //Make sure that the repository is cloned and up to date + string localRepositoryPath = GetLocalRepositoryPath(repositoryId); + + await CloneOrPull(localRepositoryPath, repositoryUrl); + + //Make sure that the file/directory exists in the repository + string relativePath = Encoding.UTF8.GetString(Convert.FromBase64String(fileId)); + if (relativePath.Contains("..")) + { + throw new ArgumentException("Invalid file id"); + } + + + string filePath = Path.Combine(localRepositoryPath, relativePath); + + string workPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + + Directory.CreateDirectory(workPath); + + + if (File.Exists(filePath)) + { + File.Copy(filePath, Path.Combine(workPath, Path.GetFileName(filePath))); + } + else if (Directory.Exists(filePath)) + { + await CopyDirectory(filePath, workPath); + } + else + { + throw new ArgumentException("File/Directory not found"); + } + + string zipPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".zip"); + + await Task.Run(() => + { + ZipFile.CreateFromDirectory(workPath, zipPath, CompressionLevel.SmallestSize, false); + }); + + Directory.Delete(workPath, true); + + return zipPath; + } + + private static async Task CopyDirectory(string sourceDirName, string destDirName) + { + DirectoryInfo dir = new DirectoryInfo(sourceDirName); + + if (!dir.Exists) + { + throw new DirectoryNotFoundException( + "Source directory does not exist or could not be found: " + + sourceDirName); + } + + DirectoryInfo[] dirs = dir.GetDirectories(); + + // Append the name of the source directory to the destination directory + destDirName = Path.Combine(destDirName, dir.Name); + + await Task.Run(() => + { + // If the destination directory doesn't exist, create it. + Directory.CreateDirectory(destDirName); + + FileInfo[] files = dir.GetFiles(); + foreach (FileInfo file in files) + { + string tempPath = Path.Combine(destDirName, file.Name); + file.CopyTo(tempPath, false); + } + }); + + foreach (DirectoryInfo subdir in dirs) + { + string tempPath = Path.Combine(destDirName, subdir.Name); + await CopyDirectory(subdir.FullName, tempPath); + } + } + + + private async Task CloneOrPull(string localRepositoryPath, string repositoryUrl) + { + if (Directory.Exists(localRepositoryPath)) + { + //Pull repository changes + var pullOptions = new PullOptions(); + pullOptions.FetchOptions = new FetchOptions(); + + await Task.Run(() => + { + using (var repository = new Repository(localRepositoryPath)) + { + Commands.Pull(repository, new Signature("x", "x@x.com", DateTimeOffset.Now), pullOptions); + } + }); + + } + else + { + //Clone repository + await Task.Run(() => + { + Repository.Clone(repositoryUrl, localRepositoryPath); + }); + } + } + + private async Task GetFileTree(string path, int relativePathPosition, List contents) + { + + foreach (var directory in Directory.GetDirectories(path)) + { + //Ignore .git folder + if (directory.Substring(relativePathPosition).StartsWith(".git")) { continue; } + + contents.Add(CreateBalsamRepoFile(directory, relativePathPosition, BalsamRepoFile.TypeEnum.FolderEnum)); + await GetFileTree(directory, relativePathPosition, contents); + } + + await Task.Run(() => + { + foreach (var file in Directory.GetFiles(path)) + { + contents.Add(CreateBalsamRepoFile(file, relativePathPosition, BalsamRepoFile.TypeEnum.FileEnum)); + } + }); + } + + private static BalsamRepoFile CreateBalsamRepoFile(string path, int relativePathPosition, BalsamRepoFile.TypeEnum type) + { + var repoFile = new BalsamRepoFile(); + repoFile.Path = path.Substring(relativePathPosition); + repoFile.Name = Path.GetFileName(path); + repoFile.Id = Convert.ToBase64String(Encoding.UTF8.GetBytes(repoFile.Path)); + repoFile.Type = type; + + return repoFile; + } + } +} diff --git a/src/Balsam/src/Balsam.Infrastructure/Repositories/KnowledgeLibraryRepository.cs b/src/Balsam/src/Balsam.Infrastructure/Repositories/KnowledgeLibraryRepository.cs new file mode 100644 index 0000000..0ceb53c --- /dev/null +++ b/src/Balsam/src/Balsam.Infrastructure/Repositories/KnowledgeLibraryRepository.cs @@ -0,0 +1,73 @@ +using Balsam.Model; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + + +namespace Balsam.Repositories +{ + public class KnowledgeLibraryRepository : IKnowledgeLibraryRepository + { + private readonly ILogger _logger; + private readonly IHubRepositoryClient _hubRepositoryClient; + private readonly IKnowledgeLibraryContentRepository _knowledgeLibraryContentRepository; + protected HubPaths HubPaths { get; set; } + + public KnowledgeLibraryRepository(ILogger logger, IHubRepositoryClient hubRepositoryClient, IKnowledgeLibraryContentRepository knowledgeLibraryContentRepository) + { + _logger = logger; + _hubRepositoryClient = hubRepositoryClient; + _knowledgeLibraryContentRepository = knowledgeLibraryContentRepository; + HubPaths = new HubPaths(_hubRepositoryClient.Path); + } + + public async Task> ListKnowledgeLibraries() + { + var knowledgeLibraries = new List(); + + var kbPath = HubPaths.GetKnowledgeLibrariesPath(); + _logger.LogDebug("kbPath: " + kbPath); + foreach (var knowledgelibraryFile in Directory.GetFiles(kbPath)) + { + try + { + var knowledgeLibrary = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(knowledgelibraryFile)); + if (knowledgeLibrary != null) + { + knowledgeLibraries.Add(knowledgeLibrary); + } + else + { + _logger.LogWarning($"Could not parse properties file for knowledgelibrary {knowledgelibraryFile}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error with deserialization of file"); + } + } + return knowledgeLibraries; + } + + public async Task GetKnowledgeLibrary(string knowledgeLibraryId) + { + return (await ListKnowledgeLibraries()).First(kb => kb.Id == knowledgeLibraryId); + } + + public async Task> GetRepositoryFileTree(string knowledgeLibraryId) + { + var knowledgeLibrary = await GetKnowledgeLibrary(knowledgeLibraryId); + + return await _knowledgeLibraryContentRepository.GetFileTree(knowledgeLibraryId, knowledgeLibrary.RepositoryUrl); + } + + public string GetRepositoryFilePath(string repositoryId, string fileId) + { + return _knowledgeLibraryContentRepository.GetFilePath(repositoryId, fileId); + } + + public async Task GetZippedResource(string repositoryId, string repositoryUrl, string fileId) + { + return await _knowledgeLibraryContentRepository.GetZippedResource(repositoryId, repositoryUrl, fileId); + } + } +} diff --git a/src/Balsam/src/Balsam.Infrastructure/Repositories/ProjectGitOpsRepository.cs b/src/Balsam/src/Balsam.Infrastructure/Repositories/ProjectGitOpsRepository.cs new file mode 100644 index 0000000..2e6f044 --- /dev/null +++ b/src/Balsam/src/Balsam.Infrastructure/Repositories/ProjectGitOpsRepository.cs @@ -0,0 +1,47 @@ +using Balsam.Model; + +namespace Balsam.Repositories +{ + public class ProjectGitOpsRepository : GitOpsRepository, IProjectGitOpsRepository + { + + public ProjectGitOpsRepository(IHubRepositoryClient hubRepoClient) + : base(hubRepoClient) + { + } + + public async Task CreateProjectManifests(BalsamProject project) + { + string projectPath = HubPaths.GetProjectPath(project.Id); + var context = new ProjectContext() { Project = project }; + + HubRepositoryClient.PullChanges(); + string projectsTemplatePath = HubPaths.GetProjectsTemplatesPath(); + await CreateManifests(context, projectPath, projectsTemplatePath); + + HubRepositoryClient.PersistChanges($"Manifests for project {project.Id} persisted"); + } + + public async Task DeleteBranchManifests(string projectId, string branchId) + { + var branchPath = HubPaths.GetBranchPath(projectId, branchId); + + HubRepositoryClient.PullChanges(); + + await DeleteManifests(branchPath); + + HubRepositoryClient.PersistChanges($"Branch {branchId} deleted for project {projectId}"); + } + + public async Task DeleteProjectManifests(string projectId) + { + var projectPath = HubPaths.GetProjectPath(projectId); + + HubRepositoryClient.PullChanges(); + + await DeleteManifests(projectPath); + + HubRepositoryClient.PersistChanges($"Project {projectId} deleted"); + } + } +} diff --git a/src/Balsam/src/Balsam.Infrastructure/Repositories/ProjectRepository.cs b/src/Balsam/src/Balsam.Infrastructure/Repositories/ProjectRepository.cs new file mode 100644 index 0000000..f7bf3e5 --- /dev/null +++ b/src/Balsam/src/Balsam.Infrastructure/Repositories/ProjectRepository.cs @@ -0,0 +1,327 @@ +using Balsam.Model; +using Balsam.Utility; +using Newtonsoft.Json; +using File = System.IO.File; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Balsam.Repositories +{ + public class ProjectRepository : IProjectRepository + { + private readonly IHubRepositoryClient _hubRepositoryClient; + private readonly ILogger _logger; + private readonly CapabilityOptions _git; + + protected HubPaths HubPaths { get; set; } + + public ProjectRepository(IOptionsSnapshot capabilityOptions, ILogger logger, IHubRepositoryClient hubRepositoryClient) + { + _git = capabilityOptions.Get(Capabilities.Git); + _logger = logger; + _hubRepositoryClient = hubRepositoryClient; + HubPaths = new HubPaths(_hubRepositoryClient.Path); + } + + public async Task> GetProjects(bool includeBranches = true) + { + var projects = new List(); + //var hubPath = Path.Combine(_hubRepositoryClient.Path, "hub"); + var hubPath = HubPaths.GetHubPath(); + + foreach (var projectPath in Directory.GetDirectories(hubPath)) + { + var propsFile = Path.Combine(projectPath, "properties.json"); + var project = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); + if (project != null) + { + if (includeBranches) + { + project.Branches = await GetBranches(project.Id); + } + projects.Add(project); + } + else + { + _logger.LogWarning($"Could not parse properties file {propsFile}"); + } + } + return projects; + } + + //TODO:Move to other repository + public async Task GetFile(string projectId, string branchId, string fileId) + { + var project = await GetProject(projectId); + var branch = await GetBranch(projectId, branchId); + if (project is null || branch is null || project.Git is null) + { + throw new ArgumentException("File not found"); + } + + try + { + + //TODO:Use Git provider API... + var request = new HttpRequestMessage(HttpMethod.Get, $"{_git.ServiceLocation}/repos/{project.Git.Id}/branches/{branch.GitBranch}/files/{fileId}"); + + var httpClient = new HttpClient(); + + var response = await httpClient.SendAsync(request); + if (response.IsSuccessStatusCode) + { + var data = await response.Content.ReadAsByteArrayAsync(); + return new FileContent(data, response.Content.Headers.ContentType?.MediaType ?? "application/octet-stream"); + } + } + catch (Exception ex) + { + string errorMessage = "Could not read files from repository"; + _logger.LogError(ex, errorMessage); + throw new ApplicationException(errorMessage); + } + + return null; + } + + public async Task GetProject(string projectId, bool includeBranches = true) + { + //var projectPath = Path.Combine(_hubRepositoryClient.Path, "hub", projectId); + var projectPath = HubPaths.GetProjectPath(projectId); + + var propsFile = Path.Combine(projectPath, "properties.json"); + + if (!File.Exists(propsFile)) + { + var errorMessage = $"Project with project id {projectId} does not exist"; + _logger.LogError(errorMessage); + return null; + } + + var project = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); + + if (project == null) + { + var errorMessage = $"Project with project id {projectId} does not exist"; + _logger.LogError(errorMessage); + throw new ArgumentException(errorMessage); + } + + if (includeBranches) + { + project.Branches = await GetBranches(project.Id); + } + + return project!; + } + + private async Task> GetBranches(string projectId) + { + var branches = new List(); + + var projectPath = HubPaths.GetProjectPath(projectId); + if (!Directory.Exists(projectPath)) + { + var errorMessage = $"Project with project id {projectId} does not exist"; + _logger.LogError(errorMessage); + throw new ArgumentException(errorMessage); + } + + foreach (var branchPath in Directory.GetDirectories(projectPath)) + { + var propsFile = Path.Combine(branchPath, "properties.json"); + var branch = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); + if (branch != null) + { + branches.Add(branch); + } + else + { + _logger.LogWarning($"Could not parse properties file {propsFile}"); + } + } + + //var gitBranches = await _repositoryApi.GetBranchesAsync(repositoryId); + + //var gitBranchesWithoutBalsamBranch = gitBranches.Where(gitBranch => !branches.Any(b => b.GitBranch == gitBranch)); + + //foreach (var gitBranch in gitBranchesWithoutBalsamBranch) + //{ + // var newBalsamBranch = await CreateBalsamBranchFromGitBranch(projectId, gitBranch); + // if (newBalsamBranch != null) + // { + // branches.Add(newBalsamBranch); + // } + // else + // { + // _logger.LogWarning($"Could not create BalsamBranch from git-branch {gitBranch} for project {projectId}"); + // } + //} + + return branches; + } + + + public async Task GetBranch(string projectId, string branchId) + { + var propsFile = Path.Combine(HubPaths.GetBranchPath(projectId, branchId), "properties.json"); + + if (!File.Exists(propsFile)) + { + return null; + } + + var branch = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); + return branch; + } + + + public async Task ProjectExists(string projectName) + { + var projects = await GetProjects(false); //TODO: Optimize + if (projects.FirstOrDefault(p => p.Name == projectName) == null) + { + return false; + } + return true; + } + + public async Task CreateProject(BalsamProject project, string defaultBranchName) + { + _logger.LogDebug($"create project information"); + + var id = NameUtil.SanitizeName(project.Name); + + project.Id = id; + + await AddOrUpdate(project); + + var branch = await CreateBranch(project, defaultBranchName, project.Description, true); + + if (branch != null) + { + _logger.LogInformation($"Default Balsam branch {defaultBranchName} created"); + project.Branches.Add(branch); + } + else + { + throw new ApplicationException($"Could not create branch for project {project.Id}."); + } + + return project; + } + + private async Task AddOrUpdate(BalsamProject project) + { + string projectPath = HubPaths.GetProjectPath(project.Id); + + _hubRepositoryClient.PullChanges(); + _logger.LogDebug($"Assure path exists {projectPath}"); + + DirectoryUtil.AssureDirectoryExists(projectPath); + + string propPath = Path.Combine(projectPath, "properties.json"); + // serialize JSON to a string and then write string to a file + await File.WriteAllTextAsync(propPath, JsonConvert.SerializeObject(project)); + _hubRepositoryClient.PersistChanges($"New program with id {project.Id}"); + } + + public async Task UpdateProject(BalsamProject project) + { + await AddOrUpdate(project); + } + + + public async Task CreateBranch(string projectId, string branchName, string description) + { + var project = await GetProject(projectId, false); + + if (project is null) + { + throw new ArgumentException($"Project with id {projectId} does not exist", projectId); + } + + var createdBranch = await CreateBranch(project, branchName, description, false); + return createdBranch; + } + + private async Task CreateBranch(BalsamProject project, string branchName, string description, bool isDefault = false) + { + var branchId = NameUtil.SanitizeName(branchName); + var branchPath = HubPaths.GetBranchPath(project.Id, branchId); + + DirectoryUtil.AssureDirectoryExists(branchPath); + + var branch = new BalsamBranch() + { + Id = branchId, + Name = branchName, + Description = description, + IsDefault = isDefault, + GitBranch = branchName + }; + + var propPath = Path.Combine(branchPath, "properties.json"); + + _hubRepositoryClient.PullChanges(); + await File.WriteAllTextAsync(propPath, JsonConvert.SerializeObject(branch)); + _hubRepositoryClient.PersistChanges($"Branch {branchName} created for project {project.Name}"); + + return branch; + } + + public async Task CreateBalsamBranchFromGitBranch(string projectId, string gitBranchName) + { + var project = await GetProject(projectId, false); + + if (project is null || project.Git is null) + { + return null; + } + + var createdBranch = await CreateBranch(project, gitBranchName, "", false); + + return createdBranch; + } + + public async Task DeleteBranch(string projectId, string branchId) + { + var branchPath = HubPaths.GetBranchPath(projectId, branchId); + + var propsFile = Path.Combine(branchPath, "properties.json"); + + _hubRepositoryClient.PullChanges(); + + if (File.Exists(propsFile)) + { + await Task.Run(() => File.Delete(propsFile)); + } + + _hubRepositoryClient.PersistChanges($"Branch {branchId} deleted for project {projectId}"); + + } + + public async Task DeleteProject(string projectId) + { + var projectPath = Path.Combine(_hubRepositoryClient.Path, "hub", projectId); + + _hubRepositoryClient.PullChanges(); + + var propPath = Path.Combine(projectPath, "properties.json"); + + if (!Directory.Exists(projectPath)) + { + return; + } + + if (File.Exists(propPath)) + { + await Task.Run(() => File.Delete(propPath)); + } + + _hubRepositoryClient.PersistChanges($"Project {projectId} deleted"); + } + + + } +} diff --git a/src/Balsam/src/Balsam.Infrastructure/Repositories/WorkspaceGitOpsRepository.cs b/src/Balsam/src/Balsam.Infrastructure/Repositories/WorkspaceGitOpsRepository.cs new file mode 100644 index 0000000..eed997a --- /dev/null +++ b/src/Balsam/src/Balsam.Infrastructure/Repositories/WorkspaceGitOpsRepository.cs @@ -0,0 +1,57 @@ +using Balsam.Model; +using Balsam.Utility; + +namespace Balsam.Repositories +{ + public class WorkspaceGitOpsRepository : GitOpsRepository, IWorkspaceGitOpsRepository + { + public static string WorkspaceUrlAnnotationKey = "balsam-workspace-url"; + + public WorkspaceGitOpsRepository(IHubRepositoryClient hubRepoClient) + : base(hubRepoClient) + { + } + + public async Task CreateWorkspaceManifests(BalsamProject project, BalsamBranch branch, BalsamWorkspace workspace, UserInfo user, string templateId) + { + var workspacePath = HubPaths.GetWorkspacePath(project.Id, branch.Id, user.UserName, workspace.Id); + var context = new WorkspaceContext(project, branch, workspace, user); + + HubRepositoryClient.PullChanges(); + + string workspaceTemplatesPath = HubPaths.GetWorkspaceTemplatesPath(templateId); + await CreateManifests(context, workspacePath, workspaceTemplatesPath); + + HubRepositoryClient.PersistChanges($"Manifests for workspace {workspace.Id} persisted"); + + } + + public async Task DeleteWorkspaceManifests(string projectId, string branchId, string workspaceId, string userName) + { + var workspacePath = HubPaths.GetWorkspacePath(projectId, branchId, userName, workspaceId); + + HubRepositoryClient.PullChanges(); + + await DeleteManifests(workspacePath); + + HubRepositoryClient.PersistChanges($"Deleted workspace manifests with id {workspaceId}"); + } + + public async Task GetWorkspaceUrl(string projectId, string branchId, string userName, string workspaceId, string urlConfigFile) + { + //TODO: Pull? + + return await Task.Run(() => + { + string manifestFilePath = GetWorkspaceManifestFilePath(projectId, branchId, userName, workspaceId, urlConfigFile); + + return ManifestUtil.GetAnnotation(manifestFilePath, WorkspaceUrlAnnotationKey); + }); + } + + private string GetWorkspaceManifestFilePath(string projectId, string branchId, string userName, string workspaceId, string manifestFileName) + { + return Path.Combine(HubPaths.GetWorkspacePath(projectId, branchId, userName, workspaceId), manifestFileName); + } + } +} diff --git a/src/Balsam/src/Balsam.Infrastructure/Repositories/WorkspaceRepository.cs b/src/Balsam/src/Balsam.Infrastructure/Repositories/WorkspaceRepository.cs new file mode 100644 index 0000000..23f5f7d --- /dev/null +++ b/src/Balsam/src/Balsam.Infrastructure/Repositories/WorkspaceRepository.cs @@ -0,0 +1,302 @@ +using Balsam.Utility; +using Newtonsoft.Json; +using Balsam.Model; +using Microsoft.Extensions.Logging; +using System.ComponentModel.Design; + +namespace Balsam.Repositories +{ + + public class WorkspaceRepository : IWorkspaceRepository + { + private readonly IHubRepositoryClient _hubRepositoryClient; + private readonly ILogger _logger; + protected HubPaths HubPaths { get; set; } + + public WorkspaceRepository(IHubRepositoryClient hubRepositoryClient, ILogger logger) + { + _hubRepositoryClient = hubRepositoryClient; + _logger = logger; + HubPaths = new HubPaths(_hubRepositoryClient.Path); + } + + public async Task CreateWorkspace(BalsamWorkspace workspace) + { + var projectPath = HubPaths.GetProjectPath(workspace.ProjectId); + + if (!Directory.Exists(projectPath)) + { + DirectoryUtil.AssureDirectoryExists(projectPath); + } + + var workspacePath = HubPaths.GetWorkspacePath(workspace.ProjectId, workspace.BranchId, workspace.Owner, workspace.Id); + var propPath = Path.Combine(workspacePath, "properties.json"); + + _logger.LogDebug("Pulling changes"); + _hubRepositoryClient.PullChanges(); + DirectoryUtil.AssureDirectoryExists(workspacePath); + // serialize JSON to a string and then write string to a file + await File.WriteAllTextAsync(propPath, JsonConvert.SerializeObject(workspace)); + _hubRepositoryClient.PersistChanges($"New workspace with id {workspace.Id}"); + + _logger.LogInformation("Workspace created"); + return workspace; + } + + public async Task GetWorkspaceTemplate(string templateId) + { + //var templatesPaths = HubPaths.GetTemplatesPath(); + var workspaceTemplatePath = HubPaths.GetWorkspaceTemplatesPath(templateId); + + var templateFilePath = Path.Combine(workspaceTemplatePath, "properties.json"); + + //TODO: Pull? + + if (!File.Exists(templateFilePath)) + { + return null; + } + + var content = await File.ReadAllTextAsync(templateFilePath); + + return JsonConvert.DeserializeObject(content); + } + + public async Task DeleteWorkspace(string projectId, string branchId, string workspaceId, string userName) + { + _hubRepositoryClient.PullChanges(); + var workspacePath = HubPaths.GetWorkspacePath(projectId, branchId, userName, workspaceId); + + var propPath = Path.Combine(workspacePath, "properties.json"); + + if (!Directory.Exists(workspacePath)) + { + return; + } + + if (File.Exists(propPath)) + { + await Task.Run(() => File.Delete(propPath)); + } + + _hubRepositoryClient.PersistChanges($"Deleted workspace with id {workspaceId}"); + } + + public async Task GetWorkspace(string projectId, string userId, string workspaceId) + { + var workspaces = await GetWorkspacesByProjectAndUser(projectId, userId); + return workspaces.FirstOrDefault(w => w.Id == workspaceId); + } + + public string GenerateWorkspaceId(string name) + { + return NameUtil.SanitizeName(name); + } + + public async Task> ListWorkspaceTemplates() + { + var workspaceTemplatesPath = HubPaths.GetWorkspacesTemplatesPath(); + + if (!Directory.Exists(workspaceTemplatesPath)) + { + return new List(); + } + + var workspaceTemplates = await Task.Run(() => + { + var workspaceTemplates = new List(); + foreach (var directory in Directory.GetDirectories(workspaceTemplatesPath)) + { + var id = new DirectoryInfo(directory).Name; + var fileName = Path.Combine(directory, "properties.json"); + var jsonString = File.ReadAllText(fileName); + var template = JsonConvert.DeserializeObject(jsonString); + + if (template == null) continue; + + template.Id = id; + workspaceTemplates.Add(template); + } + return workspaceTemplates; + }); + + + return workspaceTemplates; + } + + public async Task> GetWorkspaces() + { + var hubPath = HubPaths.GetHubPath(); + + var workspaces = new List(); + + //TODO: Gör oberoende av katalogstruktur + foreach (var projectPath in Directory.GetDirectories(hubPath)) + { + foreach (var branchPath in Directory.GetDirectories(projectPath)) + { + foreach (var userPath in Directory.GetDirectories(branchPath)) + { + foreach (var workspacePath in Directory.GetDirectories(userPath)) + { + var propsFile = Path.Combine(workspacePath, "properties.json"); + + if (File.Exists(propsFile)) + { + var content = await File.ReadAllTextAsync(propsFile); + var workspace = JsonConvert.DeserializeObject(content); + + if (workspace != null) + { + workspaces.Add(workspace); + } + else + { + continue; + } + } + else + { + continue; + } + } + } + } + } + return workspaces; + } + + public async Task> GetWorkspacesByUser(string userId, List projectIds) + { + var workspaces = new List(); + + foreach (var projectId in projectIds) + { + var projectPath = HubPaths.GetProjectPath(projectId); + + //TODO: Gör oberoende av katalogstruktur? + if (Directory.Exists(projectPath)) + { + foreach (var branchPath in Directory.GetDirectories(projectPath)) + { + var userPath = Path.Combine(branchPath, userId); + + if (Directory.Exists(userPath)) + { + foreach (var workspacePath in Directory.GetDirectories(userPath)) + { + var propsFile = Path.Combine(workspacePath, "properties.json"); + var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); + + if (workspace == null) continue; + workspaces.Add(workspace); + } + } + } + } + } + return workspaces; + } + + public async Task> GetWorkspacesByProject(string projectId) + { + + var workspaces = new List(); + + var projectPath = HubPaths.GetProjectPath(projectId); + //TODO: Gör oberoende av katalogstruktur + if (Directory.Exists(projectPath)) + { + foreach (var branchPath in Directory.GetDirectories(projectPath)) + { + foreach (var userPath in Directory.GetDirectories(branchPath)) + { + foreach (var workspacePath in Directory.GetDirectories(userPath)) + { + var propsFile = Path.Combine(workspacePath, "properties.json"); + var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); + + if (workspace == null) continue; + workspaces.Add(workspace); + } + } + } + } + return workspaces; + } + + public async Task> GetWorkspacesByProjectAndBranch(string projectId, string branchId) + { + //var hubPath = Path.Combine(_hubRepositoryClient.Path, "hub"); + + var workspaces = new List(); + + var branchPath = HubPaths.GetBranchPath(projectId, branchId); + + if (Directory.Exists(branchPath)) + { + foreach (var userPath in Directory.GetDirectories(branchPath)) + { + foreach (var workspacePath in Directory.GetDirectories(userPath)) + { + var propsFile = Path.Combine(workspacePath, "properties.json"); + var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); + + if (workspace == null) continue; + workspaces.Add(workspace); + } + } + } + return workspaces; + } + + public async Task> GetWorkspacesByProjectAndUser(string projectId, string userId) + { + var workspaces = new List(); + + var projectPath = HubPaths.GetProjectPath(projectId); + + if (Directory.Exists(projectPath)) + { + foreach (var branchPath in Directory.GetDirectories(projectPath)) + { + var userPath = Path.Combine(branchPath, userId); + if (Directory.Exists(userPath)) + { + foreach (var workspacePath in Directory.GetDirectories(userPath)) + { + var propsFile = Path.Combine(workspacePath, "properties.json"); + var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); + + if (workspace == null) continue; + workspaces.Add(workspace); + } + } + } + } + return workspaces; + } + + public async Task> GetWorkspacesByProjectBranchAndUser(string projectId, string branchId, string userId) + { + var hubPath = HubPaths.GetHubPath(); + + var workspaces = new List(); + + var userPath = Path.Combine(hubPath, projectId, branchId, userId); + + if (Directory.Exists(userPath)) + { + foreach (var workspacePath in Directory.GetDirectories(userPath)) + { + var propsFile = Path.Combine(workspacePath, "properties.json"); + var workspace = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(propsFile)); + + if (workspace == null) continue; + workspaces.Add(workspace); + } + } + return workspaces; + } + } +} diff --git a/src/Balsam/src/Balsam.Api/DirectoryUtil.cs b/src/Balsam/src/Balsam.Infrastructure/Utility/DirectoryUtil.cs similarity index 93% rename from src/Balsam/src/Balsam.Api/DirectoryUtil.cs rename to src/Balsam/src/Balsam.Infrastructure/Utility/DirectoryUtil.cs index a9715ea..bd2cd74 100644 --- a/src/Balsam/src/Balsam.Api/DirectoryUtil.cs +++ b/src/Balsam/src/Balsam.Infrastructure/Utility/DirectoryUtil.cs @@ -1,6 +1,6 @@ -namespace Balsam.Api +namespace Balsam.Utility { - internal static class DirectoryUtil + public static class DirectoryUtil { ////Removes all content in the directory diff --git a/src/Balsam/src/Balsam.Api/ManifestUtil.cs b/src/Balsam/src/Balsam.Infrastructure/Utility/ManifestUtil.cs similarity index 84% rename from src/Balsam/src/Balsam.Api/ManifestUtil.cs rename to src/Balsam/src/Balsam.Infrastructure/Utility/ManifestUtil.cs index 8ab1ec0..696661d 100644 --- a/src/Balsam/src/Balsam.Api/ManifestUtil.cs +++ b/src/Balsam/src/Balsam.Infrastructure/Utility/ManifestUtil.cs @@ -1,8 +1,7 @@ using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization; -using BalsamApi.Server.Models; -namespace Balsam.Api +namespace Balsam.Utility { public class ManifestUtil { @@ -26,13 +25,12 @@ internal class Metadata public Metadata() { - + } } - public static string GetWorkspaceUrl(string manifestFilePath) + public static string GetAnnotation(string manifestFilePath, string annotationKey) { - if (!File.Exists(manifestFilePath)) { return ""; @@ -49,9 +47,9 @@ public static string GetWorkspaceUrl(string manifestFilePath) manifest = deserializer.Deserialize(manifestContent); } - if (manifest != null && manifest.Metadata.Annotations.ContainsKey("balsam-workspace-url")) + if (manifest != null && manifest.Metadata.Annotations.ContainsKey(annotationKey)) { - return manifest.Metadata.Annotations["balsam-workspace-url"]; + return manifest.Metadata.Annotations[annotationKey]; } return ""; } diff --git a/src/Balsam/src/Balsam.Tests/Api/Test_ProjectController.cs b/src/Balsam/src/Balsam.Tests/Api/Test_ProjectController.cs new file mode 100644 index 0000000..c263a1c --- /dev/null +++ b/src/Balsam/src/Balsam.Tests/Api/Test_ProjectController.cs @@ -0,0 +1,579 @@ +using Balsam.Api.Controllers; +using Balsam.Interfaces; +using Balsam.Model; +using Balsam.Tests.Helpers; +using BalsamApi.Server.Models; +using GitProviderApiClient.Api; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +//using Castle.Core.Logging; +using Moq; +using System.Security.Claims; +using BranchCreatedResponse = BalsamApi.Server.Models.BranchCreatedResponse; +using CreateBranchRequest = BalsamApi.Server.Models.CreateBranchRequest; + +namespace Balsam.Tests.Application; + +public class Test_ProjectController +{ + + [Fact] + public async void Test_CreateProject() + { + var loggerMock = new Mock>(); + var capabilityOptionsSnapshotMock = TestHelpers.CreateCapabilityOptionsSnapshotMock(true, true, true, true); + + var projectServiceMock = new Mock(); + + string projectName = "projectName"; + + + projectServiceMock.Setup(m => m.CreateProject(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync((string preferredName, string description, string defaultBranchName, string username, string? sourceLocation = null) => + { + var project = TestHelpers.NewBalsamProject(preferredName, description); + project.Branches.Add(TestHelpers.NewBalsamBranch(defaultBranchName, true)); + project.Oidc.GroupName = projectName; + return project; + }); + + + var knowledgeLibraryServiceMock = new Mock(); + var repositoryApiMock = new Mock(); + + var projectController = new ProjectController(capabilityOptionsSnapshotMock.Object, + loggerMock.Object, + projectServiceMock.Object, + knowledgeLibraryServiceMock.Object, + repositoryApiMock.Object); + + var testUserName = "test_user"; + var claims = new[] + { + + new Claim("preferred_username", testUserName), + new Claim("groups", "test_group") + }; + + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + projectController.ControllerContext.HttpContext = new DefaultHttpContext { User = user }; + + + var createProjectRequest = new CreateProjectRequest + { + BranchName = "branchName", + Description = "description", + Name = "projectName", + }; + + var response = await projectController.CreateProject(createProjectRequest); + + projectServiceMock.Verify(m => m.CreateProject(It.Is(name => name == createProjectRequest.Name), + It.Is(description => description == createProjectRequest.Description), + It.Is(branchName => branchName == createProjectRequest.BranchName), + It.Is(userName => userName == testUserName), + It.Is(sourceLocation => sourceLocation == null))); + + Assert.NotNull(response); + + var okObjectResult = response as OkObjectResult; + Assert.NotNull(okObjectResult); + + var projectResponse = okObjectResult.Value as ProjectCreatedResponse; + Assert.NotNull(projectResponse); + + Assert.Equal(createProjectRequest.Name, projectResponse.Name); + + } + + + [Fact] + public async void Test_CreateProject_ProjectExist() + { + var loggerMock = new Mock>(); + var capabilityOptionsSnapshotMock = TestHelpers.CreateCapabilityOptionsSnapshotMock(true, true, true, true); + + var projectServiceMock = new Mock(); + + projectServiceMock.Setup(m => m.ProjectExists(It.IsAny())) + .ReturnsAsync(() => true); + + + + var knowledgeLibraryServiceMock = new Mock(); + var repositoryApiMock = new Mock(); + + var projectController = new ProjectController(capabilityOptionsSnapshotMock.Object, + loggerMock.Object, + projectServiceMock.Object, + knowledgeLibraryServiceMock.Object, + repositoryApiMock.Object); + + var testUserName = "test_user"; + var claims = new[] + { + + new Claim("preferred_username", testUserName), + new Claim("groups", "test_group") + }; + + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + projectController.ControllerContext.HttpContext = new DefaultHttpContext { User = user }; + + + var sameName = "projectName"; + + var createProjectRequest = new CreateProjectRequest + { + BranchName = "branchName1", + Description = "description1", + Name = sameName, + }; + + + var response = await projectController.CreateProject(createProjectRequest); + + projectServiceMock.Verify(m => m.CreateProject(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + + Assert.NotNull(response as BadRequestObjectResult); + } + + + [Theory] + [InlineData("projectGroup", "")] + [InlineData("projectGroup", "otherGroup")] + [InlineData("projectGroup", "projectGroup")] + public async void Test_CrateBranch_With_Authentication(string projectGroupName, string userGroupName) + { + var loggerMock = new Mock>(); + var capabilityOptionsSnapshotMock = TestHelpers.CreateCapabilityOptionsSnapshotMock(true, true, true, true); + + var projectServiceMock = new Mock(); + + + var project = TestHelpers.NewBalsamProject("project"); + project.Oidc.GroupName = projectGroupName; + + projectServiceMock.Setup(m => m.GetProject(It.IsAny(), + It.IsAny())) + .ReturnsAsync((string projectId, bool includeBranches) => project); + + projectServiceMock.Setup(m => m.CreateBranch(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync((string projectId, string fromBranch, string branchName, string description) + => TestHelpers.NewBalsamBranch(branchName, false, description)); + + + var knowledgeLibraryServiceMock = new Mock(); + var repositoryApiMock = new Mock(); + + var projectController = new ProjectController(capabilityOptionsSnapshotMock.Object, + loggerMock.Object, + projectServiceMock.Object, + knowledgeLibraryServiceMock.Object, + repositoryApiMock.Object); + + var testUserName = "test_user"; + var claims = new[] + { + + new Claim("preferred_username", testUserName), + new Claim("groups", userGroupName) + }; + + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + projectController.ControllerContext.HttpContext = new DefaultHttpContext { User = user }; + + + var createBranchRequest = new CreateBranchRequest + { + FromBranch = "fromBranch", + Name = "name", + Description = "description" + }; + + var response = await projectController.CreateBranch(project.Id, createBranchRequest); + + bool userInGroup = projectGroupName == userGroupName; + + Times verifyCallTimes = userInGroup ? Times.Once() : Times.Never(); + + + projectServiceMock.Verify(m => m.CreateBranch(It.Is(projectId => projectId == project.Id), + It.Is(fromBranch => fromBranch == createBranchRequest.FromBranch), + It.Is(branchName => branchName == createBranchRequest.Name), + It.Is(description => description == createBranchRequest.Description)), + verifyCallTimes); + + Assert.NotNull(response); + + if (userInGroup) + { + var okObjectResult = response as OkObjectResult; + Assert.NotNull(okObjectResult); + + var branchResponse = okObjectResult.Value as BranchCreatedResponse; + Assert.NotNull(branchResponse); + + Assert.Equal(createBranchRequest.Name, branchResponse.Name); + } + else + { + Assert.NotNull(response as UnauthorizedObjectResult); + } + + } + + + [Fact] + public async void Test_GetFiles() + { + var loggerMock = new Mock>(); + var capabilityOptionsSnapshotMock = TestHelpers.CreateCapabilityOptionsSnapshotMock(true, true, true, true); + + var projectServiceMock = new Mock(); + + var project = TestHelpers.NewBalsamProject("project1"); + var branch = TestHelpers.NewBalsamBranch("branch1", true); + project.Branches.Add(branch); + + var file1 = TestHelpers.NewBalsamRepoFile("file1", BalsamRepoFile.TypeEnum.FileEnum); + var folder1 = TestHelpers.NewBalsamRepoFile("folder1", BalsamRepoFile.TypeEnum.FolderEnum); + + var files = new List + { + file1, + folder1 + }; + + projectServiceMock.Setup(m => m.GetGitBranchFiles(It.IsAny(), + It.IsAny())) + .ReturnsAsync(() => files); + + + var knowledgeLibraryServiceMock = new Mock(); + var repositoryApiMock = new Mock(); + + var projectController = new ProjectController(capabilityOptionsSnapshotMock.Object, + loggerMock.Object, + projectServiceMock.Object, + knowledgeLibraryServiceMock.Object, + repositoryApiMock.Object); + + var response = await projectController.GetFiles(project.Id, branch.Id); + + projectServiceMock.Verify(m => m.GetGitBranchFiles(It.Is(projectId => projectId == project.Id), + It.Is(branchId => branchId == branch.Id))); + + Assert.NotNull(response); + + var okObjectResult = response as OkObjectResult; + Assert.NotNull(okObjectResult); + + var actualFiles = okObjectResult.Value as IEnumerable; + Assert.NotNull(actualFiles); + + TestHelpers.AssertRepoFile(file1, actualFiles.First(f => f.Id == file1.Id)); + TestHelpers.AssertRepoFile(folder1, actualFiles.First(f => f.Id == folder1.Id)); + + } + + [Fact] + public async void Test_GetFile() + { + var loggerMock = new Mock>(); + var capabilityOptionsSnapshotMock = TestHelpers.CreateCapabilityOptionsSnapshotMock(true, true, true, true); + + var projectServiceMock = new Mock(); + + var project = TestHelpers.NewBalsamProject("project1"); + var branch = TestHelpers.NewBalsamBranch("branch1", true); + project.Branches.Add(branch); + + var expectedFileId = "file1"; + + var byteArray = new byte[] { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }; + var mediaType = "application/octet-stream"; + + var fileContent = new FileContent(byteArray, mediaType); + + + projectServiceMock.Setup(m => m.GetFile(It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(() => fileContent); + + + var knowledgeLibraryServiceMock = new Mock(); + var repositoryApiMock = new Mock(); + + var projectController = new ProjectController(capabilityOptionsSnapshotMock.Object, + loggerMock.Object, + projectServiceMock.Object, + knowledgeLibraryServiceMock.Object, + repositoryApiMock.Object); + + var testUserName = "test_user"; + var claims = new[] + { + + new Claim("preferred_username", testUserName), + new Claim("groups", "group1") + }; + + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + projectController.ControllerContext.HttpContext = new DefaultHttpContext { User = user }; + + var response = await projectController.GetFile(project.Id, branch.Id, expectedFileId); + + projectServiceMock.Verify(m => m.GetFile(It.Is(projectId => projectId == project.Id), + It.Is(branchId => branchId == branch.Id), + It.Is(fileId => fileId == expectedFileId))); + + Assert.NotNull(response); + + var actualFile = response as FileContentResult; + Assert.NotNull(actualFile); + + Assert.Equal(byteArray, actualFile.FileContents); + + } + + [Theory] + [InlineData("projectGroup", "")] + [InlineData("projectGroup", "otherGroup")] + [InlineData("projectGroup", "projectGroup")] + public async void Test_DeleteBranch_With_Authentication(string projectGroupName, string userGroupName) + { + var loggerMock = new Mock>(); + var capabilityOptionsSnapshotMock = TestHelpers.CreateCapabilityOptionsSnapshotMock(true, true, true, true); + + var projectServiceMock = new Mock(); + + + var project = TestHelpers.NewBalsamProject("project"); + var branch = TestHelpers.NewBalsamBranch("branch1", true); + + project.Branches.Add(branch); + project.Oidc.GroupName = projectGroupName; + + projectServiceMock.Setup(m => m.GetProject(It.IsAny(), + It.IsAny())) + .ReturnsAsync((string projectId, bool includeBranches) => project); + + + var knowledgeLibraryServiceMock = new Mock(); + var repositoryApiMock = new Mock(); + + var projectController = new ProjectController(capabilityOptionsSnapshotMock.Object, + loggerMock.Object, + projectServiceMock.Object, + knowledgeLibraryServiceMock.Object, + repositoryApiMock.Object); + + var testUserName = "test_user"; + var claims = new[] + { + + new Claim("preferred_username", testUserName), + new Claim("groups", userGroupName) + }; + + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + projectController.ControllerContext.HttpContext = new DefaultHttpContext { User = user }; + + + var response = await projectController.DeleteBranch(project.Id, branch.Id); + + bool userInGroup = projectGroupName == userGroupName; + + Times verifyCallTimes = userInGroup ? Times.Once() : Times.Never(); + + + projectServiceMock.Verify(m => m.DeleteBranch(It.Is(projectId => projectId == project.Id), + It.Is(branchId => branchId == branch.Id)), + verifyCallTimes); + + Assert.NotNull(response); + + if (userInGroup) + { + var okResult = response as OkResult; + Assert.NotNull(okResult); + } + else + { + Assert.NotNull(response as UnauthorizedObjectResult); + } + + } + + [Theory] + [InlineData("projectGroup", "")] + [InlineData("projectGroup", "otherGroup")] + [InlineData("projectGroup", "projectGroup")] + public async void Test_DeleteProject_With_Authentication(string projectGroupName, string userGroupName) + { + var loggerMock = new Mock>(); + var capabilityOptionsSnapshotMock = TestHelpers.CreateCapabilityOptionsSnapshotMock(true, true, true, true); + + var projectServiceMock = new Mock(); + + + var project = TestHelpers.NewBalsamProject("project"); + var branch = TestHelpers.NewBalsamBranch("branch1", true); + + project.Branches.Add(branch); + project.Oidc.GroupName = projectGroupName; + + projectServiceMock.Setup(m => m.GetProject(It.IsAny(), + It.IsAny())) + .ReturnsAsync((string projectId, bool includeBranches) => project); + + + var knowledgeLibraryServiceMock = new Mock(); + var repositoryApiMock = new Mock(); + + var projectController = new ProjectController(capabilityOptionsSnapshotMock.Object, + loggerMock.Object, + projectServiceMock.Object, + knowledgeLibraryServiceMock.Object, + repositoryApiMock.Object); + + var testUserName = "test_user"; + var claims = new[] + { + + new Claim("preferred_username", testUserName), + new Claim("groups", userGroupName) + }; + + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + projectController.ControllerContext.HttpContext = new DefaultHttpContext { User = user }; + + var response = await projectController.DeleteProject(project.Id); + + bool userInGroup = projectGroupName == userGroupName; + + Times verifyCallTimes = userInGroup ? Times.Once() : Times.Never(); + + + projectServiceMock.Verify(m => m.DeleteProject(It.Is(projectId => projectId == project.Id)), + verifyCallTimes); + + Assert.NotNull(response); + + if (userInGroup) + { + var okResult = response as OkResult; + Assert.NotNull(okResult); + } + else + { + Assert.NotNull(response as UnauthorizedObjectResult); + } + + } + + [Theory] + [InlineData("projectGroup", "")] + [InlineData("projectGroup", "otherGroup")] + [InlineData("projectGroup", "projectGroup")] + public async void Test_CopyFromKnowledgeLibrary_With_Authentication(string projectGroupName, string userGroupName) + { + var loggerMock = new Mock>(); + var capabilityOptionsSnapshotMock = TestHelpers.CreateCapabilityOptionsSnapshotMock(true, true, true, true); + + var projectServiceMock = new Mock(); + + + var project = TestHelpers.NewBalsamProject("project"); + var branch = TestHelpers.NewBalsamBranch("branch1", true); + + project.Branches.Add(branch); + project.Oidc.GroupName = projectGroupName; + + projectServiceMock.Setup(m => m.GetProject(It.IsAny(), + It.IsAny())) + .ReturnsAsync((string projectId, bool includeBranches) => project); + + + var file = TestHelpers.NewBalsamRepoFile("file1", BalsamRepoFile.TypeEnum.FileEnum); + + var knowledgeLibraryServiceMock = new Mock(); + var knowledgeLibrary = TestHelpers.NewBalsamKnowledgeLibrary("kb1", "The knowledge library"); + + knowledgeLibraryServiceMock.Setup(m => m.GetKnowledgeLibrary(It.Is(libraryId => libraryId == knowledgeLibrary.Id))) + .ReturnsAsync(() => knowledgeLibrary); + + + var repositoryApiMock = new Mock(); + + var projectController = new ProjectController(capabilityOptionsSnapshotMock.Object, + loggerMock.Object, + projectServiceMock.Object, + knowledgeLibraryServiceMock.Object, + repositoryApiMock.Object); + + var testUserName = "test_user"; + var claims = new[] + { + + new Claim("preferred_username", testUserName), + new Claim("groups", userGroupName) + }; + + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + projectController.ControllerContext.HttpContext = new DefaultHttpContext { User = user }; + + + var response = await projectController.CopyFromKnowleadgeLibrary(project.Id, + branch.Id, + knowledgeLibrary.Id, + file.Id); + + bool userInGroup = projectGroupName == userGroupName; + + Times verifyCallTimes = userInGroup ? Times.Once() : Times.Never(); + + + projectServiceMock.Verify(m => m.CopyFromKnowledgeLibrary(It.Is(projectId => projectId == project.Id), + It.Is(branchId => branchId == branch.Id), + It.Is(libraryId => libraryId == knowledgeLibrary.Id), + It.Is(fileId => fileId == file.Id)), + verifyCallTimes); + + Assert.NotNull(response); + + if (userInGroup) + { + var okResult = response as OkResult; + Assert.NotNull(okResult); + } + else + { + Assert.NotNull(response as UnauthorizedObjectResult); + } + + } +} \ No newline at end of file diff --git a/src/Balsam/src/Balsam.Tests/Api/Test_WorkspaceController.cs b/src/Balsam/src/Balsam.Tests/Api/Test_WorkspaceController.cs new file mode 100644 index 0000000..e73069d --- /dev/null +++ b/src/Balsam/src/Balsam.Tests/Api/Test_WorkspaceController.cs @@ -0,0 +1,458 @@ +using Balsam.Api.Controllers; +using Balsam.Interfaces; +using Balsam.Model; +using Balsam.Tests.Helpers; +using BalsamApi.Server.Models; +using LibGit2Sharp; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +//using Castle.Core.Logging; +using Moq; +using System.Security.Claims; + +namespace Balsam.Tests.Application; + +public class Test_WorkspaceController +{ + [Theory] + [InlineData("projectGroup", "")] + [InlineData("projectGroup", "otherGroup")] + [InlineData("projectGroup", "projectGroup")] + public async void Test_CreateWorkspace_With_Authentication(string projectGroupName, string userGroupName) + { + var loggerMock = new Mock>(); + var capabilityOptionsSnapshotMock = TestHelpers.CreateCapabilityOptionsSnapshotMock(true, true, true, true); + var projectServiceMock = new Mock(); + var project = TestHelpers.NewBalsamProject("project1"); + var branch = TestHelpers.NewBalsamBranch("branch1", true); + var template = TestHelpers.NewWorkspaceTemplate("template1"); + var testUserName = "test_user"; + var testEmail = "x@y.z"; + + project.Branches.Add(branch); + project.Oidc.GroupName = projectGroupName; + + projectServiceMock.Setup(m => m.GetProject(It.IsAny(), + It.IsAny())) + .ReturnsAsync((string projectId, bool includeBranches) => project); + + var workspace = TestHelpers.NewBalsamWorkspace("expectedWorkspace"); + workspace.ProjectId = project.Id; + workspace.BranchId = branch.Id; + workspace.TemplateId = template.Id; + workspace.Owner = testUserName; + workspace.Url = "http://balsam.here.com"; + + var createWorkspaceRequest = new CreateWorkspaceRequest + { + Name = workspace.Name, + BranchId = workspace.BranchId, + ProjectId = workspace.ProjectId, + TemplateId = workspace.TemplateId, + }; + + + var workspaceServiceMock = new Mock(); + workspaceServiceMock.Setup(m => m.CreateWorkspace(It.Is(projectId => projectId == createWorkspaceRequest.ProjectId), + It.Is(branchId => branchId == createWorkspaceRequest.BranchId), + It.Is(name => name == createWorkspaceRequest.Name), + It.Is(templateId => templateId == createWorkspaceRequest.TemplateId), + It.Is(userName => userName == testUserName), + It.Is(userMail => userMail == testEmail))) + .ReturnsAsync(() => workspace); + + + var workspaceController = new WorkspaceController(loggerMock.Object, + projectServiceMock.Object, + workspaceServiceMock.Object); + + + var claims = new[] + { + + new Claim("preferred_username", testUserName), + new Claim("groups", userGroupName), + new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", testEmail) + + }; + + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + workspaceController.ControllerContext.HttpContext = new DefaultHttpContext { User = user }; + + + + var response = await workspaceController.CreateWorkspace(createWorkspaceRequest); + + bool userInGroup = projectGroupName == userGroupName; + + Times verifyCallTimes = userInGroup ? Times.Once() : Times.Never(); + + workspaceServiceMock.Verify(m => m.CreateWorkspace(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + verifyCallTimes); + + Assert.NotNull(response); + + if (userInGroup) + { + var okObjectResult = response as OkObjectResult; + Assert.NotNull(okObjectResult); + + var workspaceResponse = okObjectResult.Value as WorkspaceCreatedResponse; + Assert.NotNull(workspaceResponse); + + AssertResult(workspace, workspaceResponse); + + } + else + { + Assert.NotNull(response as UnauthorizedObjectResult); + } + + } + + private static void AssertResult(BalsamWorkspace expectedWorkspace, WorkspaceCreatedResponse actualWorkspace) + { + Assert.Equal(expectedWorkspace.Name, actualWorkspace.Name); + Assert.Equal(expectedWorkspace.BranchId, actualWorkspace.BranchId); + Assert.Equal(expectedWorkspace.ProjectId, actualWorkspace.ProjectId); + Assert.Equal(expectedWorkspace.Url, actualWorkspace.Url); + } + + [Theory] + [InlineData("projectGroup", "")] + [InlineData("projectGroup", "otherGroup")] + [InlineData("projectGroup", "projectGroup")] + public async void Test_DeleteWorkspace_With_Authentication(string projectGroupName, string userGroupName) + { + var loggerMock = new Mock>(); + var capabilityOptionsSnapshotMock = TestHelpers.CreateCapabilityOptionsSnapshotMock(true, true, true, true); + var projectServiceMock = new Mock(); + var project = TestHelpers.NewBalsamProject("project1"); + var branch = TestHelpers.NewBalsamBranch("branch1", true); + var template = TestHelpers.NewWorkspaceTemplate("template1"); + var testUserName = "test_user"; + var testEmail = "x@y.z"; + + project.Branches.Add(branch); + project.Oidc.GroupName = projectGroupName; + + projectServiceMock.Setup(m => m.GetProject(It.IsAny(), + It.IsAny())) + .ReturnsAsync((string projectId, bool includeBranches) => project); + + var workspace = TestHelpers.NewBalsamWorkspace("expectedWorkspace"); + workspace.ProjectId = project.Id; + workspace.BranchId = branch.Id; + workspace.TemplateId = template.Id; + workspace.Owner = testUserName; + workspace.Url = "http://balsam.here.com"; + + var workspaceServiceMock = new Mock(); + workspaceServiceMock.Setup(m => m.GetWorkspace(It.Is(projectId => projectId == workspace.ProjectId), + It.Is(userName => userName == workspace.Owner), + It.Is(workspaceId => workspaceId == workspace.Id))) + .ReturnsAsync(() => workspace); + + var workspaceController = new WorkspaceController(loggerMock.Object, + projectServiceMock.Object, + workspaceServiceMock.Object); + + var claims = new[] + { + + new Claim("preferred_username", testUserName), + new Claim("groups", userGroupName), + new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", testEmail) + + }; + + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + workspaceController.ControllerContext.HttpContext = new DefaultHttpContext { User = user }; + + var response = await workspaceController.DeleteWorkspace(workspace.Id, workspace.ProjectId, workspace.BranchId); + + bool userInGroup = projectGroupName == userGroupName; + + Times verifyCallTimes = userInGroup ? Times.Once() : Times.Never(); + + workspaceServiceMock.Verify(m => m.DeleteWorkspace(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + verifyCallTimes); + + Assert.NotNull(response); + + if (userInGroup) + { + var okObjectResult = response as OkObjectResult; + Assert.NotNull(okObjectResult); + } + else + { + Assert.NotNull(response as UnauthorizedObjectResult); + } + } + + [Fact] + public async void Test_ListTempates() + { + var loggerMock = new Mock>(); + var projectServiceMock = new Mock(); + var workspaceServiceMock = new Mock(); + + var template1 = TestHelpers.NewWorkspaceTemplate("template1"); + var template2 = TestHelpers.NewWorkspaceTemplate("template2"); + + var listTemplates = new List { + template1, + template2 + }; + + workspaceServiceMock.Setup(m => m.ListWorkspaceTemplates()) + .ReturnsAsync(() => listTemplates); + + var workspaceController = new WorkspaceController(loggerMock.Object, + projectServiceMock.Object, + workspaceServiceMock.Object); + + var response = await workspaceController.ListTemplates(); + + workspaceServiceMock.Verify(m => m.ListWorkspaceTemplates()); + + Assert.NotNull(response); + + var okObjectResult = response as OkObjectResult; + Assert.NotNull(okObjectResult); + + var actualTemplates = okObjectResult.Value as IEnumerable