feat: initial commit

This commit is contained in:
Thom Lamb
2026-06-23 11:18:54 -05:00
parent fc3ce9a074
commit 1bb980f070
790 changed files with 149255 additions and 218 deletions
@@ -0,0 +1,9 @@
using System;
namespace Strata.Analytics.Api.Authorization
{
public class AuthorizationOptions
{
public Guid AuthorizedDbGuid { get; set; }
}
}
@@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using System;
using System.Threading.Tasks;
using Strata.CoreLib.Claims.Extensions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
namespace Strata.Analytics.Api.Authorization
{
public class AuthorizedDbMinimumRoleHandler : AuthorizationHandler<AuthorizedDbMinimumRoleRequirement>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IWebHostEnvironment _env;
private Guid _authorizedDbGuid;
public AuthorizedDbMinimumRoleHandler(IHttpContextAccessor httpContextAccessor, IWebHostEnvironment env, IOptions<AuthorizationOptions> options)
{
_httpContextAccessor = httpContextAccessor;
_env = env;
_authorizedDbGuid = options.Value.AuthorizedDbGuid;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthorizedDbMinimumRoleRequirement requirement)
{
var user = _httpContextAccessor.HttpContext.User;
var dbGuid = user.GetDatabaseGuid();
if (!_env.IsProduction() || dbGuid == _authorizedDbGuid)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
context.Fail();
return Task.CompletedTask;
}
}
}
@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authorization;
namespace Strata.Analytics.Api.Authorization
{
public class AuthorizedDbMinimumRoleRequirement : IAuthorizationRequirement
{
}
}
@@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Strata.Analytics.Biz.Extracts.Services;
using Strata.Analytics.Biz.Extracts.Models;
using Strata.CoreLib.Claims.Extensions;
namespace Strata.Analytics.Api.Authorization
{
public class CustomSQLStrataOnlyRoleHandler : AuthorizationHandler<ExtractCustomSqlStrataOnlyRoleRequirement>
{
private readonly IExtractService _extractService;
private readonly IHttpContextAccessor _httpContextAccessor;
public CustomSQLStrataOnlyRoleHandler(IExtractService extractService, IHttpContextAccessor httpContextAccessor)
{
_extractService = extractService;
_httpContextAccessor = httpContextAccessor;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ExtractCustomSqlStrataOnlyRoleRequirement requirement)
{
var extractIdObj = _httpContextAccessor.HttpContext!.GetRouteData()?.Values["extractId"];
if (extractIdObj == null)
{
context.Fail();
return;
}
var extractId = int.Parse(extractIdObj.ToString()!);
var extract =
await _extractService.GetByIdAsync(extractId, System.Threading.CancellationToken.None);
if (extract == null)
{
context.Fail();
return;
}
if (extract.ExtractType == ExtractType.CustomSql && !context.User.IsSdtEmployee())
{
context.Fail();
return;
}
context.Succeed(requirement);
return;
}
}
}
@@ -0,0 +1,92 @@
using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks;
using Strata.Analytics.Biz.DataSources.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Strata.CoreLib.Claims.Extensions;
using System;
using System.Collections.Generic;
namespace Strata.Analytics.Api.Authorization
{
public class DataSourceMinimumRoleHandler : AuthorizationHandler<DataSourceMinimumRoleRequirement>
{
private readonly IDataSourceService _dataSourceService;
private readonly IHttpContextAccessor _httpContextAccessor;
public DataSourceMinimumRoleHandler(IDataSourceService dataSourceService, IHttpContextAccessor httpContextAccessor)
{
_dataSourceService = dataSourceService;
_httpContextAccessor = httpContextAccessor;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, DataSourceMinimumRoleRequirement requirement)
{
var dataSourceId = await GetDataSourceId(context);
if (!dataSourceId.HasValue)
{
context.Fail();
return;
}
var dataSourceRole = await _dataSourceService.GetCurrentUserRoleAsync(dataSourceId.Value, System.Threading.CancellationToken.None);
if (!dataSourceRole.HasValue || dataSourceRole.Value < requirement.MinimumRole)
{
context.Fail();
return;
}
context.Succeed(requirement);
}
private async Task<int?> GetDataSourceId(AuthorizationHandlerContext context)
{
// Check route for dataSourceId
var dataSourceIdObj = _httpContextAccessor.HttpContext.GetRouteData()?.Values["dataSourceId"];
if (dataSourceIdObj == null)
{
// Check body for dataSourceId
var req = _httpContextAccessor.HttpContext.Request;
try
{
req.EnableBuffering();
req.Body.Position = 0;
var body = await req.ReadFromJsonAsync<Dictionary<string, dynamic>>(null);
req.Body.Position = 0;
if (req.Body.Length > 0)
{
foreach (var kvp in body)
if (string.Equals(kvp.Key, "dataSourceId", StringComparison.InvariantCultureIgnoreCase))
dataSourceIdObj = kvp.Value;
}
else
return null;
}
catch
{
return null;
}
}
if (!int.TryParse(dataSourceIdObj.ToString(), out int dataSourceId))
return null;
return dataSourceId;
}
// Swallows throw when user does not have SdtEmployee claim and returns false
private bool IsSdtEmployee(AuthorizationHandlerContext context)
{
var isSdtEmployee = false;
try
{
isSdtEmployee = context.User.IsSdtEmployee();
}
catch { }
return isSdtEmployee;
}
}
}
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Strata.Analytics.Biz.DataSources.Models;
namespace Strata.Analytics.Api.Authorization
{
public class DataSourceMinimumRoleRequirement : IAuthorizationRequirement
{
public DataSourceRole MinimumRole { get; set; }
public DataSourceMinimumRoleRequirement(DataSourceRole role)
{
MinimumRole = role;
}
}
}
@@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Authorization;
public class ExtractCustomSqlStrataOnlyRoleRequirement : IAuthorizationRequirement
{
public ExtractCustomSqlStrataOnlyRoleRequirement()
{
}
}
@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Strata.Analytics.Biz.Extracts.Services;
namespace Strata.Analytics.Api.Authorization
{
public class ExtractMinimumRoleHandler : AuthorizationHandler<ExtractMinimumRoleRequirement>
{
private readonly IExtractService _extractService;
private readonly IHttpContextAccessor _httpContextAccessor;
public ExtractMinimumRoleHandler(IExtractService extractService, IHttpContextAccessor httpContextAccessor)
{
_extractService = extractService;
_httpContextAccessor = httpContextAccessor;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ExtractMinimumRoleRequirement requirement)
{
var extractIdObj = _httpContextAccessor.HttpContext!.GetRouteData()?.Values["extractId"];
if (extractIdObj == null)
{
context.Fail();
return;
}
var extractId = int.Parse(extractIdObj.ToString()!);
var extractRole = await _extractService.GetCurrentUserRoleAsync(extractId, System.Threading.CancellationToken.None);
var extract =
await _extractService.GetByIdAsync(extractId, System.Threading.CancellationToken.None);
if (extract == null)
{
context.Fail();
return;
}
if (!extractRole.HasValue || extractRole.Value < requirement.MinimumRole)
{
context.Fail();
return;
}
context.Succeed(requirement);
}
}
}
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Strata.Analytics.Biz.Extracts.Models;
namespace Strata.Analytics.Api.Authorization
{
public class ExtractMinimumRoleRequirement : IAuthorizationRequirement
{
public ExtractRole MinimumRole { get; set; }
public ExtractMinimumRoleRequirement(ExtractRole role)
{
MinimumRole = role;
}
}
}
@@ -0,0 +1,16 @@
namespace Strata.Analytics.Api.Authorization
{
public class Policies
{
public const string StrataOnly = "StrataOnly";
public const string IsAdmin = "IsAdmin";
public const string DataSourceViewer = "DataSourceViewer";
public const string DataSourceEditor = "DataSourceEditor";
public const string WorkbookViewer = "WorkbookViewer";
public const string WorkbookEditor = "WorkbookEditor";
public const string ExtractViewer = "ExtractViewer";
public const string ExtractEditor = "ExtractEditor";
public const string AuthorizedDbOnly = "AuthorizedDbOnly";
public const string CustomSqlAccess = "CustomSqlAccess";
}
}
@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Strata.Analytics.Biz.Workbooks.Services;
namespace Strata.Analytics.Api.Authorization
{
public class WorkbookMinimumRoleHandler : AuthorizationHandler<WorkbookMinimumRoleRequirement>
{
private readonly IWorkbookService _workbookService;
private readonly IHttpContextAccessor _httpContextAccessor;
public WorkbookMinimumRoleHandler(IWorkbookService workbookService, IHttpContextAccessor httpContextAccessor)
{
_workbookService = workbookService;
_httpContextAccessor = httpContextAccessor;;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, WorkbookMinimumRoleRequirement requirement)
{
var workbookIdObj = _httpContextAccessor.HttpContext.GetRouteData()?.Values["workbookId"];
if (workbookIdObj == null)
{
context.Fail();
return;
}
var workbookId = int.Parse(workbookIdObj.ToString());
var workbookRole = await _workbookService.GetCurrentUserRoleAsync(workbookId, System.Threading.CancellationToken.None);
if (!workbookRole.HasValue || workbookRole.Value < requirement.MinimumRole)
{
context.Fail();
return;
}
context.Succeed(requirement);
}
}
}
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Authorization;
using Strata.Analytics.Models.Workbooks;
namespace Strata.Analytics.Api.Authorization
{
public class WorkbookMinimumRoleRequirement : IAuthorizationRequirement
{
public WorkbookRole MinimumRole { get; set; }
public WorkbookMinimumRoleRequirement(WorkbookRole role)
{
MinimumRole = role;
}
}
}
@@ -0,0 +1,122 @@
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Strata.Analytics.Biz.ContentMigration.Services;
using Strata.Analytics.Biz.ContentMigration.Models;
using Microsoft.AspNetCore.Http;
using System;
using Microsoft.AspNetCore.Authorization;
using Strata.Analytics.Api.Authorization;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Strata.Analytics.Biz.ContentMigration.Models.MigrationPlan;
namespace Strata.Analytics.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class ContentMigrationController : ControllerBase
{
private readonly IContentMigrationService _contentMigrationService;
public ContentMigrationController(IContentMigrationService contentMigrationService)
{
_contentMigrationService = contentMigrationService;
}
[HttpGet("legacy-reports")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IEnumerable<ReReportDisplay>> GetAdHocReports(CancellationToken cancellationToken)
{
return await _contentMigrationService.GetAdHocReports(cancellationToken);
}
[HttpPost("{reportGuid}/migrate")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<AdhocMigrationResult>> MigrateReport([FromRoute] Guid reportGuid, [FromQuery, BindRequired] int workbookId, [FromQuery, BindRequired] int dataSourceId, [FromBody] Plan plan, CancellationToken cancellationToken)
{
var migratedReport = await _contentMigrationService.MigrateReport(reportGuid, workbookId, dataSourceId, plan, cancellationToken);
if (migratedReport == null)
{
return NotFound();
}
return Ok(migratedReport);
}
[HttpPost("{reportGuid}/migrateExtract")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ExtractMigrationResult>> MigrateReportToExtract([FromRoute] Guid reportGuid, [FromQuery, BindRequired] int dataSourceId, [FromBody] Plan plan, CancellationToken cancellationToken)
{
var migratedReport = await _contentMigrationService.MigrateReportToExtract(reportGuid, dataSourceId, plan, cancellationToken);
if (migratedReport == null)
{
return NotFound();
}
return Ok(migratedReport);
}
[HttpGet("migrationPlan")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Plan>> GetMigrationPlan([FromQuery, BindRequired] Guid reportGuid, [FromQuery, BindRequired] int dataSourceId, CancellationToken cancellationToken)
{
var migrationPlan = await _contentMigrationService.GetMigrationPlan(reportGuid, dataSourceId, cancellationToken);
if (migrationPlan == null)
{
return NotFound();
}
return Ok(migrationPlan);
}
[HttpPut("dataMappings")]
[Authorize(Policy = Policies.StrataOnly)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<DataMapping>> UpdateDataMapping([FromBody] DataMapping dataMapping, CancellationToken cancellationToken)
{
var updatedDataMapping = await _contentMigrationService.UpdateDataMapping(dataMapping, cancellationToken);
if (updatedDataMapping == null)
{
return NotFound();
}
return Ok(updatedDataMapping);
}
[HttpGet("dataMappings")]
[Authorize(Policy = Policies.StrataOnly)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<DataMapping>>> GetDataMappings(CancellationToken cancellationToken)
{
var dataMappings = await _contentMigrationService.GetDataMappings(cancellationToken);
return Ok(dataMappings);
}
[HttpPost("dataMappings")]
[Authorize(Policy = Policies.StrataOnly)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddDataMappings([FromBody] IEnumerable<DataMapping> dataMappings, CancellationToken cancellationToken)
{
await _contentMigrationService.AddDataMappings(dataMappings, cancellationToken);
return NoContent();
}
[HttpDelete("dataMappings/{dataMappingId}")]
[Authorize(Policy = Policies.StrataOnly)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DeleteDataMapping([FromRoute] int dataMappingId, CancellationToken cancellationToken)
{
var mapping = await _contentMigrationService.GetDataMapping(dataMappingId, cancellationToken);
if (mapping == null)
{
return NotFound();
}
await _contentMigrationService.DeleteDataMapping(mapping, cancellationToken);
return NoContent();
}
}
}
@@ -0,0 +1,241 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Strata.Analytics.Api.Authorization;
using Strata.Analytics.Biz.DataQuery;
using Strata.Analytics.Biz.DataSources.Models;
using Strata.Analytics.Biz.DataSources.Services;
using Strata.Analytics.Biz.Export.Models;
using Strata.Analytics.Biz.Export.Services;
using Strata.Analytics.Models.Utils;
using Strata.DataSchema.Models.Query;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.Analytics.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class DataSourcesController : ControllerBase
{
private readonly IDataSourceService _dataSourceService;
private readonly IDataQueryService _dataQueryService;
private readonly IExportService _exportService;
private readonly ILogger<DataSourcesController> _logger;
public DataSourcesController(IDataSourceService dataSourceService, IDataQueryService dataQueryService, IExportService exportService, ILogger<DataSourcesController> logger)
{
_dataSourceService = dataSourceService;
_dataQueryService = dataQueryService;
_exportService = exportService;
_logger = logger;
}
[HttpGet]
public async Task<IEnumerable<DataSource>> GetDataSourcesAsync(CancellationToken cancellationToken)
{
return await _dataSourceService.GetAllAsync(cancellationToken);
}
[HttpGet("{dataSourceId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<DataSource>> GetById(int dataSourceId, CancellationToken cancellationToken)
{
var dataSource = await _dataSourceService.GetByIdAsync(dataSourceId, cancellationToken);
if (dataSource == null)
{
return NotFound();
}
return Ok(dataSource);
}
[HttpPut("{dataSourceId}")]
[Authorize(Policy = Policies.DataSourceEditor)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<DataSource>> Update(int dataSourceId, DataSourceSaveInfo dataSourceSaveInfo, CancellationToken cancellationToken)
{
var isFound = await _dataSourceService.ContainsAsync(dataSourceId, cancellationToken);
if (!isFound)
{
return NotFound();
}
var updatedDataSource = await _dataSourceService.UpdateAsync(dataSourceId, dataSourceSaveInfo, cancellationToken);
return Ok(updatedDataSource);
}
[HttpPut("")]
[Authorize(Policy = Policies.DataSourceEditor)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<DataSource>> Save(DataSourceSaveInfo dataSourceSaveInfo, CancellationToken cancellationToken)
{
var isFound = await _dataSourceService.ContainsAsync(dataSourceSaveInfo.DataSourceId, cancellationToken);
if (!isFound)
{
return NotFound();
}
var updatedDataSource = await _dataSourceService.SaveAsync(dataSourceSaveInfo, cancellationToken);
return Ok(updatedDataSource);
}
[HttpDelete("{dataSourceId}")]
[Authorize(Policy = Policies.DataSourceEditor)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
public async Task<IActionResult> Delete(int dataSourceId, CancellationToken cancellationToken)
{
await _dataSourceService.DeleteAsync(dataSourceId, cancellationToken);
return NoContent();
}
[HttpPost("")]
[Authorize(Policy = Policies.StrataOnly)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<DataSource>> Create(DataSourceSaveInfo dataSourceSaveInfo, CancellationToken cancellationToken)
{
return await _dataSourceService.CreateAsync(dataSourceSaveInfo, cancellationToken);
}
[HttpPost("{dataSourceId}/tables/{dataTableId}")]
[Authorize(Policy = Policies.DataSourceEditor)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
public async Task<ActionResult<DataSourceTable>> AddDataTableLink([FromRoute] int dataSourceId, [FromRoute] int dataTableId, CancellationToken cancellationToken)
{
return await _dataSourceService.AddDataTableAsync(dataSourceId, dataTableId, cancellationToken);
}
[HttpDelete("{dataSourceId}/tables/{dataTableId}")]
[Authorize(Policy = Policies.DataSourceEditor)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
public async Task<ActionResult> DeleteDataTableLink([FromRoute] int dataSourceId, [FromRoute] int dataTableId, CancellationToken cancellationToken)
{
await _dataSourceService.DeleteDataTableAsync(dataSourceId, dataTableId, cancellationToken);
return NoContent();
}
[HttpGet("{dataSourceId}/role-assignments")]
[Authorize(Policy = Policies.DataSourceViewer)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<DataSourceRoleAssignment>>> GetRoleAssignments([FromRoute] int dataSourceId, CancellationToken cancellationToken)
{
return Ok(await _dataSourceService.GetRoleAssignmentsAsync(new List<int> { dataSourceId }, cancellationToken));
}
[HttpPost("{dataSourceId}/role-assignments/batch")]
[Authorize(Policy = Policies.DataSourceEditor)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<DataSourceRoleAssignment>>> SaveRoleAssignments([FromRoute] int dataSourceId, [FromBody] Batch<DataSourceRoleAssignmentSaveInfo, Guid> roleAssignments, CancellationToken cancellationToken)
{
return Ok(await _dataSourceService.SaveRoleAssignmentsAsync(dataSourceId, roleAssignments, cancellationToken));
}
[HttpPost("{dataSourceId}/datacolumn/values")]
[Authorize(Policy = Policies.DataSourceViewer)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<dynamic>>> GetColumnValues([FromRoute] int dataSourceId, [FromBody] ColumnQueryConfig columnQueryConfig, CancellationToken cancellationToken)
{
try
{
return Ok(await _dataQueryService.GetColumnValues(columnQueryConfig, cancellationToken));
}
catch (UnauthorizedAccessException)
{
var dataColumnIds = new List<int> { columnQueryConfig.DataColumnId };
if (columnQueryConfig.Filter != null)
dataColumnIds.Add(columnQueryConfig.Filter.DataColumnId);
_logger.LogWarning(
"Attempted to {method} for data source {dataSourceId} with the following data column ids {dataColumnIds}",
nameof(GetColumnValues),
dataSourceId,
dataColumnIds);
return Forbid();
}
}
[HttpPost("{dataSourceId}/query")]
[Authorize(Policy = Policies.DataSourceViewer)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<dynamic>>> QueryData([FromRoute] int dataSourceId, [FromBody] QueryConfig query, CancellationToken cancellationToken)
{
try
{
return Ok(await _dataQueryService.QueryData(query, cancellationToken));
}
catch (UnauthorizedAccessException)
{
_logger.LogWarning(
"Attempted to {method} for data source {dataSourceId} with the following data column ids {dataColumnIds}",
nameof(QueryData),
dataSourceId,
query.GetAllColumnIds());
return Forbid();
}
}
[HttpPost("query")]
[Authorize(Policy = Policies.StrataOnly)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<SqlResponse>> GetSqlQuery(QueryConfig queryConfig, CancellationToken cancellationToken)
{
try
{
return Ok(await _dataQueryService.BuildSqlQuery(queryConfig, cancellationToken));
}
catch (UnauthorizedAccessException)
{
_logger.LogWarning(
"Attempted to {method} with the following data column ids {dataColumnIds}",
nameof(GetSqlQuery),
queryConfig.GetAllColumnIds());
return Forbid();
}
}
[HttpPost("{dataSourceId}/download")]
[Authorize(Policy = Policies.DataSourceViewer)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
public async Task<ActionResult> DownloadDataSheet([FromRoute] int dataSourceId, [FromBody] ExportDataSheetRequest request, CancellationToken cancellationToken)
{
var stream = await _exportService.DownloadDataSheet(request, cancellationToken);
return File(stream, "application/octet-stream");
}
}
}
@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Mvc;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using Strata.Analytics.Biz.Database;
namespace Strata.Analytics.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class DatabasesController : ControllerBase
{
private readonly IDatabaseService _databaseService;
public DatabasesController(IDatabaseService databaseService)
{
_databaseService = databaseService;
}
[HttpGet("")]
public async Task<IEnumerable<Database>> GetDatabases(CancellationToken cancellationToken)
{
return await _databaseService.GetDatabasesAsync(cancellationToken);
}
}
}
@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Mvc;
using System.Threading;
using System.Threading.Tasks;
using Strata.Analytics.Biz.Encounters;
using Strata.Analytics.Api.Models;
namespace Strata.Analytics.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class EncountersController : ControllerBase
{
private readonly IEncounterService _encounterService;
public EncountersController(IEncounterService encounterService)
{
_encounterService = encounterService;
}
/// <summary>
/// Given an encounter record number, return the encounter id
/// </summary>
[HttpPost("encounter-id")]
public async Task<int> GetEncounterId([FromBody] EncounterInfo encounterInfo, CancellationToken cancellationToken)
{
// Needs to be POST to prevent PHI data in url string
return await _encounterService.GetEncounterIdAsync(encounterInfo.EncounterRecordNumber, cancellationToken);
}
}
}
@@ -0,0 +1,236 @@
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Strata.Analytics.Biz.Extracts.Models;
using Strata.Analytics.Biz.Extracts.Services;
using Microsoft.AspNetCore.Http;
using Hangfire;
using Microsoft.AspNetCore.Authorization;
using Strata.Analytics.Api.Authorization;
using Strata.Analytics.Models.Utils;
using System;
using Strata.Analytics.Biz.Extracts.Jobs;
using System.Security.Claims;
using Hangfire.States;
using Strata.CoreLib.Claims;
using Strata.CoreLib.Claims.Extensions;
namespace Strata.Analytics.Api.Controllers
{
[Authorize(Policy = Policies.IsAdmin)]
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class ExtractsController : ControllerBase
{
private readonly IExtractService _extractService;
private readonly IBackgroundJobClient _backgroundJobClient;
private readonly IExtractExportHistoryService _historyService;
private readonly IExtractScheduleService _extractScheduleService;
private readonly ClaimsPrincipal _user;
public ExtractsController(IExtractService extractService, IBackgroundJobClient backgroundJobClient, IExtractExportHistoryService historyService, IExtractScheduleService extractScheduleService, IClaimsPrincipalAccessor claimsPrincipalAccessor)
{
_extractService = extractService;
_backgroundJobClient = backgroundJobClient;
_historyService = historyService;
_extractScheduleService = extractScheduleService;
_user = claimsPrincipalAccessor.GetCurrentClaimsPrincipal();
}
[HttpGet]
public async Task<IEnumerable<Extract>> GetExtractsAsync(CancellationToken cancellationToken)
{
return await _extractService.GetAllAsync(cancellationToken);
}
[HttpGet("{extractId}")]
[Authorize(Policy = Policies.ExtractViewer)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<Extract>> GetExtractById(int extractId, CancellationToken cancellationToken)
{
var extract = await _extractService.GetByIdAsync(extractId, cancellationToken);
if (extract == null)
{
return NotFound();
}
return Ok(extract);
}
[HttpDelete("{extractId}")]
[Authorize(Policy = Policies.CustomSqlAccess)]
[Authorize(Policy = Policies.ExtractEditor)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<IActionResult> DeleteExtract(int extractId, CancellationToken cancellationToken)
{
if (!await _extractService.ContainsAsync(extractId, cancellationToken))
{
return NotFound();
}
await _extractScheduleService.DeleteByExtractIdAsync(extractId, cancellationToken);
await _extractService.DeleteAsync(extractId, cancellationToken);
return NoContent();
}
[HttpPost]
public async Task<ActionResult<Extract>> CreateExtract(ExtractCreateInfo extractCreateInfo, CancellationToken cancellationToken)
{
return await _extractService.CreateAsync(extractCreateInfo, cancellationToken);
}
[HttpPut("{extractId}")]
[Authorize(Policy = Policies.CustomSqlAccess)]
[Authorize(Policy = Policies.ExtractEditor)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<Extract>> UpdateExtract([FromRoute] int extractId, [FromBody] ExtractSaveInfo extractSaveInfo, CancellationToken cancellationToken)
{
if (!await _extractService.ContainsAsync(extractId, cancellationToken))
{
return NotFound();
}
return Ok(await _extractService.UpdateAsync(extractId, extractSaveInfo, cancellationToken));
}
[HttpPost("export/{extractId}")]
[ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesDefaultResponseType]
[Authorize(Policy = Policies.ExtractEditor)]
public async Task<IActionResult> ExportExtract(int extractId, [FromBody] ExtractExportRequest request, CancellationToken cancellationToken)
{
if (!await _extractService.ContainsAsync(request.ExtractId, cancellationToken))
{
return NotFound();
}
request.Timezone = _user.GetTimezone();
var jobId = _backgroundJobClient.Enqueue<IExtractExportJob>(job =>
job.ExecuteExport(request.OutputFileName, _user.GetDatabaseGuid(), _user.GetStrataId(), _user.GetUserGuid(), request, null, cancellationToken));
return Accepted(new { jobId });
}
[HttpDelete("{extractId}/history/{extractHistoryId}/cancel/")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesDefaultResponseType]
[Authorize(Policy = Policies.ExtractEditor)]
public async Task<ActionResult<ExtractExportHistory>> CancelExportExtract([FromRoute] int extractId, [FromRoute] int extractHistoryId, CancellationToken cancellationToken)
{
var history = await _historyService.CancelExtractExport(extractHistoryId, cancellationToken);
if (history == null) return NotFound();
_backgroundJobClient.ChangeState(history.JobId, new DeletedState {Reason = "Job cancelled manually" });
return Ok(history);
}
[HttpGet("history/{extractId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
[Authorize(Policy = Policies.ExtractViewer)]
public async Task<ActionResult<List<ExtractExportHistory>>> GetHistoryById(int extractId, CancellationToken cancellationToken)
{
if (!await _extractService.ContainsAsync(extractId, cancellationToken))
{
return NotFound();
}
return Ok(await _historyService.GetByExtractIdAsync(extractId, cancellationToken));
}
[HttpGet("{extractId}/role-assignments")]
[Authorize(Policy = Policies.ExtractViewer)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<ExtractRoleAssignment>>> GetRoleAssignments([FromRoute] int extractId, CancellationToken cancellationToken)
{
return Ok(await _extractService.GetRoleAssignmentsAsync(extractId, cancellationToken));
}
[HttpPost("{extractId}/role-assignments/batch")]
[Authorize(Policy = Policies.ExtractEditor)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<ExtractRoleAssignment>>> SaveRoleAssignments(
[FromRoute] int extractId, [FromBody] Batch<ExtractRoleAssignmentSaveInfo, Guid> roleAssignments, CancellationToken cancellationToken)
{
return Ok(await _extractService.SaveRoleAssignmentsAsync(extractId, roleAssignments, cancellationToken));
}
[HttpPost("{extractId}/schedule")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
[Authorize(Policy = Policies.ExtractEditor)]
public async Task<ActionResult<ExtractSchedule>> CreateExtractSchedule([FromRoute] int extractId, [FromBody] ExtractScheduleSaveInfo saveInfo, CancellationToken cancellationToken)
{
if (!await _extractService.ContainsAsync(extractId, cancellationToken))
{
return NotFound();
}
return Ok(await _extractScheduleService.CreateAsync(saveInfo, cancellationToken));
}
[HttpPut("{extractId}/schedule/{extractScheduleId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
[Authorize(Policy = Policies.ExtractEditor)]
public async Task<ActionResult<ExtractSchedule>> UpdateExtractSchedule([FromRoute] int extractId, [FromRoute] int extractScheduleId, [FromBody] ExtractScheduleSaveInfo saveInfo, CancellationToken cancellationToken)
{
if (!await _extractScheduleService.ContainsAsync(extractScheduleId, cancellationToken))
{
return NotFound();
}
return Ok(await _extractScheduleService.UpdateAsync(extractScheduleId, saveInfo, cancellationToken));
}
[HttpGet("{extractId}/schedule")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
[Authorize(Policy = Policies.ExtractEditor)]
public async Task<ActionResult<ExtractSchedule>> GetScheduleByExtractId([FromRoute] int extractId, CancellationToken cancellationToken)
{
return Ok(await _extractScheduleService.GetByExtractIdAsync(extractId, cancellationToken));
}
[HttpDelete("{extractId}/schedule/{extractScheduleId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
[Authorize(Policy = Policies.ExtractEditor)]
public async Task<ActionResult> DeleteExtractSchedule([FromRoute] int extractId, [FromRoute] int extractScheduleId, CancellationToken cancellationToken)
{
if (!await _extractScheduleService.ContainsAsync(extractScheduleId, cancellationToken))
{
return NotFound();
}
await _extractScheduleService.DeleteAsync(extractScheduleId, cancellationToken);
return Ok();
}
}
}
@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using Strata.FeatureFlags.Client;
using System.Security.Claims;
using Strata.CoreLib.Claims;
using Strata.CoreLib.Claims.Extensions;
namespace Strata.Analytics.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class FeatureController : ControllerBase
{
private readonly IFeatureFlagServiceClient _featureFlagService;
private readonly ClaimsPrincipal _user;
public FeatureController(IFeatureFlagServiceClient featureFlagService, IClaimsPrincipalAccessor claimsPrincipalAccessor)
{
_featureFlagService = featureFlagService;
_user = claimsPrincipalAccessor.GetCurrentClaimsPrincipal();
}
[HttpGet("dashboards-enabled")]
public async Task<bool> IsDashboardsEnabled()
{
return await _featureFlagService.IsEnabledAsync("analytics-dashboards", _user.GetDatabaseGuid());
}
}
}
@@ -0,0 +1,55 @@
using Microsoft.AspNetCore.Mvc;
using System.Threading;
using System.Threading.Tasks;
using Strata.Analytics.Biz.Workbooks.Services;
using Microsoft.AspNetCore.Http;
using Strata.Analytics.Models.Workbooks;
namespace Strata.Analytics.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class SavedFiltersController : ControllerBase
{
private readonly ISavedFilterService _savedFilterService;
public SavedFiltersController(ISavedFilterService savedFiltersService)
{
_savedFilterService = savedFiltersService;
}
[HttpGet("{savedFilterId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<SavedFilterInfo>> GetSavedFiltersAsync(int savedFilterId, CancellationToken cancellationToken)
{
var savedFilters = await _savedFilterService.GetSavedFiltersAsync(savedFilterId, cancellationToken);
if (savedFilters == null)
{
return NotFound();
}
return Ok(savedFilters);
}
[HttpDelete("{savedFilterId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<IActionResult> Delete(int savedFilterId, CancellationToken cancellationToken)
{
await _savedFilterService.DeleteSavedFiltersAsync(savedFilterId, cancellationToken);
return NoContent();
}
[HttpPost]
public async Task<ActionResult<int>> Create(SavedFilterInfo savedFilters, CancellationToken cancellationToken)
{
var cache = await _savedFilterService.CreateSavedFiltersAsync(savedFilters, cancellationToken);
return cache.SavedFilterId;
}
}
}
@@ -0,0 +1,279 @@
using Hangfire;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Strata.Analytics.Api.Authorization;
using Strata.Analytics.Api.Models;
using Strata.Analytics.Biz.Database;
using Strata.Analytics.Biz.DataQuery;
using Strata.Analytics.Biz.DataSources.Models;
using Strata.Analytics.Biz.Schema;
using Strata.Analytics.Biz.Schema.Jobs;
using Strata.Analytics.Biz.StandardDataSources.Jobs;
using Strata.Analytics.Biz.StandardDataSources.Models;
using Strata.Analytics.Biz.StandardDataSources.Services;
using Strata.Analytics.Models;
using Strata.Analytics.Models.Utils;
using Strata.CoreLib.Claims.Extensions;
using Strata.DataSchema.Models.Query;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Strata.Analytics.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class StandardDataSourcesController : ControllerBase
{
private readonly IStandardDataSourceService _standardDataSourceService;
private readonly IDataQueryService _dataQueryService;
private readonly IDatabaseService _databaseService;
private readonly IStandardDataSourcePublishService _standardDataSourcePublishService;
private readonly IBackgroundJobClient _backgroundJobClient;
private readonly IPublishStandardDataSourcesJob _publishStandardDataSourcesJob;
private readonly ILogger<StandardDataSourcesController> _logger;
public StandardDataSourcesController(
IStandardDataSourceService standardDataSourceService,
IDataQueryService dataQueryService,
IDatabaseService databaseService,
IStandardDataSourcePublishService standardDataSourcePublishService,
IBackgroundJobClient backgroundJobClient,
IPublishStandardDataSourcesJob publishStandardDataSourcesJob,
ILogger<StandardDataSourcesController> logger)
{
_standardDataSourceService = standardDataSourceService;
_dataQueryService = dataQueryService;
_databaseService = databaseService;
_standardDataSourcePublishService = standardDataSourcePublishService;
_backgroundJobClient = backgroundJobClient;
_publishStandardDataSourcesJob = publishStandardDataSourcesJob;
_logger = logger;
}
[HttpGet]
public async Task<IEnumerable<StandardDataSource>> GetDataSourcesAsync(bool includeTableLinks = false, CancellationToken cancellationToken = default)
{
return await _standardDataSourceService.GetAllAsync(includeTableLinks, cancellationToken: cancellationToken);
}
[HttpGet("{standardDataSourceId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<StandardDataSource>> GetById(int standardDataSourceId, CancellationToken cancellationToken)
{
var standardDataSource = await _standardDataSourceService.GetByIdAsync(standardDataSourceId, cancellationToken);
if (standardDataSource == null)
{
return NotFound();
}
return Ok(standardDataSource);
}
[HttpPut("")]
[Authorize(Policy = Policies.StrataOnly)]
[Authorize(Policy = Policies.AuthorizedDbOnly)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<StandardDataSource>> Save(StandardDataSourceSaveInfo standardDataSourceSaveInfo, CancellationToken cancellationToken)
{
var isFound = await _standardDataSourceService.ContainsAsync(standardDataSourceSaveInfo.StandardDataSourceId, cancellationToken);
if (!isFound)
{
return NotFound();
}
var updatedDataSource = await _standardDataSourceService.SaveAsync(standardDataSourceSaveInfo, cancellationToken);
return Ok(updatedDataSource);
}
[HttpDelete("{standardDataSourceId}")]
[Authorize(Policy = Policies.StrataOnly)]
[Authorize(Policy = Policies.AuthorizedDbOnly)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
public async Task<IActionResult> Delete(int standardDataSourceId, CancellationToken cancellationToken)
{
await _standardDataSourceService.DeleteAsync(standardDataSourceId, cancellationToken);
return NoContent();
}
[HttpPost("")]
[Authorize(Policy = Policies.StrataOnly)]
[Authorize(Policy = Policies.AuthorizedDbOnly)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<StandardDataSource>> Create(StandardDataSourceSaveInfo standardDataSourceSaveInfo, CancellationToken cancellationToken)
{
return await _standardDataSourceService.CreateAsync(standardDataSourceSaveInfo, cancellationToken);
}
[HttpGet("{standardDataSourceId}/role-assignments")]
[Authorize(Policy = Policies.StrataOnly)]
[Authorize(Policy = Policies.AuthorizedDbOnly)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<StandardDataSourceRoleAssignment>>> GetRoleAssignments([FromRoute] int standardDataSourceId, CancellationToken cancellationToken)
{
return Ok(await _standardDataSourceService.GetRoleAssignmentsAsync(new List<int> { standardDataSourceId }, cancellationToken));
}
[HttpPost("{standardDataSourceId}/role-assignments/batch")]
[Authorize(Policy = Policies.StrataOnly)]
[Authorize(Policy = Policies.AuthorizedDbOnly)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<DataSourceRoleAssignment>>> SaveRoleAssignments([FromRoute] int standardDataSourceId, [FromBody] Batch<StandardDataSourceRoleAssignmentSaveInfo, Guid> roleAssignments, CancellationToken cancellationToken)
{
return Ok(await _standardDataSourceService.SaveRoleAssignmentsAsync(standardDataSourceId, roleAssignments, cancellationToken));
}
[HttpPost("{standardDataSourceId}/datacolumn/values")]
[Authorize(Policy = Policies.StrataOnly)]
[Authorize(Policy = Policies.AuthorizedDbOnly)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<dynamic>>> GetColumnValues([FromRoute] int standardDataSourceId, [FromBody] ColumnQueryConfig columnQueryConfig, CancellationToken cancellationToken)
{
try
{
return Ok(await _dataQueryService.GetColumnValues(columnQueryConfig, cancellationToken));
}
catch (UnauthorizedAccessException)
{
var dataColumnIds = new List<int> { columnQueryConfig.DataColumnId };
if (columnQueryConfig.Filter != null)
dataColumnIds.Add(columnQueryConfig.Filter.DataColumnId);
_logger.LogWarning(
"Attempted to {method} for standard data source {standardDataSourceId} with the following data column ids {dataColumnIds}",
nameof(GetColumnValues),
standardDataSourceId,
dataColumnIds);
return Forbid();
}
}
[HttpPost("{standardDataSourceId}/query")]
[Authorize(Policy = Policies.StrataOnly)]
[Authorize(Policy = Policies.AuthorizedDbOnly)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<dynamic>>> QueryData([FromRoute] int standardDataSourceId, [FromBody] QueryConfig query, CancellationToken cancellationToken)
{
try
{
return Ok(await _dataQueryService.QueryData(query, cancellationToken));
}
catch (UnauthorizedAccessException)
{
_logger.LogWarning(
"Attempted to {method} for standard data source {standardDataSourceId} with the following data column ids {dataColumnsIds}",
nameof(QueryData),
standardDataSourceId,
query.GetAllColumnIds());
return Forbid();
}
}
[HttpPost("query")]
[Authorize(Policy = Policies.StrataOnly)]
[Authorize(Policy = Policies.AuthorizedDbOnly)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<SqlResponse>> GetSqlQuery(QueryConfig queryConfig, CancellationToken cancellationToken)
{
try
{
return Ok(await _dataQueryService.BuildSqlQuery(queryConfig, cancellationToken));
}
catch (UnauthorizedAccessException)
{
_logger.LogWarning(
"Attempted to {method} with the following data column ids {dataColumnIds}",
nameof(GetSqlQuery),
queryConfig.GetAllColumnIds());
return Forbid();
}
}
[HttpPost("{standardDataSourceId}/include-in-publish")]
[Authorize(Policy = Policies.StrataOnly)]
[Authorize(Policy = Policies.AuthorizedDbOnly)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<StandardDataSource>> IncludeDataSourceInPublish([FromRoute] int standardDataSourceId, CancellationToken cancellationToken)
{
return Ok(await _standardDataSourceService.SetIsIncludedInStandardModel(standardDataSourceId, true, cancellationToken));
}
[HttpPost("{standardDataSourceId}/exclude-from-publish")]
[Authorize(Policy = Policies.StrataOnly)]
[Authorize(Policy = Policies.AuthorizedDbOnly)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<StandardDataSource>> ExcludeDataSourceInPublish([FromRoute] int standardDataSourceId, CancellationToken cancellationToken)
{
return Ok(await _standardDataSourceService.SetIsIncludedInStandardModel(standardDataSourceId, false, cancellationToken));
}
[HttpPost("publish")]
[Authorize(Policy = Policies.AuthorizedDbOnly)]
public async Task<string> PublishAsync([FromBody] PublishConfig publishConfig, CancellationToken cancellationToken)
{
if (publishConfig.StrataId == 0)
{
var databases = await _databaseService.GetDatabasesAsync(cancellationToken);
return _backgroundJobClient.Enqueue<PublishStandardDataSourcesAllClientsJob>(job => job.ExecuteAsync(databases.ToList(), User.GetUsername(), DateTime.UtcNow, CancellationToken.None));
}
if (Debugger.IsAttached && publishConfig.StrataId == User.GetStrataId())
{
await _publishStandardDataSourcesJob.ExecuteAsync(User.GetDatabaseGuid(), User.GetStrataId(), Guid.NewGuid().ToString(), User.GetUsername(), DateTime.UtcNow, cancellationToken);
return "";
}
var database = await _databaseService.GetDatabaseByStrataId(publishConfig.StrataId, cancellationToken);
var parentJobId = _backgroundJobClient.Enqueue<PublishStandardDataSourcesJob>(job => job.ExecuteAsync(database.DatabaseGuid, database.StrataId, Guid.NewGuid().ToString(), User.GetUsername(), DateTime.UtcNow, CancellationToken.None));
return parentJobId;
}
[HttpGet("publish-history")]
public async Task<ActionResult<IEnumerable<PublicationEvent>>> GetPublicationHistory(CancellationToken cancellationToken)
{
var publications = await _standardDataSourcePublishService.GetPublicationsAsync(cancellationToken);
return Ok(publications
.GroupBy(x => x.RequestCorrelationId)
.Select(x => new PublicationEvent
{
RequestDate = x.FirstOrDefault().RequestDate,
RequestedBy = x.FirstOrDefault().RequestedBy,
StandardDataSourceIds = x.Select(y => y.StandardDataSourceId).Distinct(),
StrataIds = x.Select(y => y.ClientId).Distinct()
}));
}
}
}
@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Strata.Users.Client;
using Strata.Users.Client.UserGroups.Models;
using System.Linq;
namespace Strata.Analytics.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class UserGroupsController : ControllerBase
{
private readonly IUsersServiceClient _usersService;
public UserGroupsController(IUsersServiceClient usersService)
{
_usersService = usersService;
}
[HttpGet]
public async Task<IEnumerable<UserGroup>> GetUserGroups(CancellationToken cancellationToken)
{
var userGroups = await _usersService.GetAllUserGroupsAsync(cancellationToken);
return userGroups.Where(ug => !ug.IsHidden);
}
}
}
@@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Strata.Users.Client;
using Strata.Users.Client.Users.Models;
namespace Strata.Analytics.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class UsersController : ControllerBase
{
private readonly IUsersServiceClient _usersService;
public UsersController(IUsersServiceClient usersService)
{
_usersService = usersService;
}
[HttpGet]
public async Task<IEnumerable<User>> GetUsers(CancellationToken cancellationToken)
{
return await _usersService.GetAllUsersAsync(cancellationToken);
}
}
}
@@ -0,0 +1,109 @@
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Strata.Analytics.Biz.Workbooks.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authorization;
using Strata.Analytics.Api.Authorization;
using Strata.Analytics.Models.Utils;
using System;
using Strata.Analytics.Models.Workbooks;
namespace Strata.Analytics.Api.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{api-version:apiVersion}/[controller]")]
public class WorkbooksController : ControllerBase
{
private readonly IWorkbookService _workbookService;
public WorkbooksController(IWorkbookService workbookService)
{
_workbookService = workbookService;
}
[HttpGet]
public async Task<IEnumerable<Workbook>> GetWorkbooksAsync(CancellationToken cancellationToken)
{
return await _workbookService.GetAllAsync(cancellationToken);
}
[HttpGet("{workbookId}")]
[Authorize(Policy = Policies.WorkbookViewer)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<Workbook>> GetById(int workbookId, CancellationToken cancellationToken)
{
var workbook = await _workbookService.GetByIdAsync(workbookId, cancellationToken);
if (workbook == null)
{
return NotFound();
}
return Ok(workbook);
}
// Delete should return 404 if id doesn't exist. Use Contains instead of loading entire object into memory to check for existence.
[HttpDelete("{workbookId}")]
[Authorize(Policy = Policies.WorkbookEditor)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<IActionResult> Delete(int workbookId, CancellationToken cancellationToken)
{
if (!await _workbookService.ContainsAsync(workbookId, cancellationToken))
{
return NotFound();
}
await _workbookService.DeleteAsync(workbookId, cancellationToken);
return NoContent();
}
[HttpPost]
public async Task<ActionResult<Workbook>> Create(WorkbookCreateInfo workbookCreateInfo, CancellationToken cancellationToken)
{
return await _workbookService.CreateAsync(workbookCreateInfo, cancellationToken);
}
[HttpPut("{workbookId}")]
[Authorize(Policy = Policies.WorkbookEditor)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<Workbook>> UpdateWorkbook([FromRoute] int workbookId, [FromBody] WorkbookSaveInfo workbookSaveInfo, CancellationToken cancellationToken)
{
if (!await _workbookService.ContainsAsync(workbookId, cancellationToken))
{
return NotFound();
}
return await _workbookService.UpdateAsync(workbookId, workbookSaveInfo, cancellationToken);
}
[HttpGet("{workbookId}/role-assignments")]
[Authorize(Policy = "WorkbookViewer")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<WorkbookRoleAssignment>>> GetRoleAssignments([FromRoute] int workbookId, CancellationToken cancellationToken)
{
return Ok(await _workbookService.GetRoleAssignmentsAsync(workbookId, cancellationToken));
}
[HttpPost("{workbookId}/role-assignments/batch")]
[Authorize(Policy = Policies.WorkbookEditor)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<WorkbookRoleAssignment>>> SaveRoleAssignments([FromRoute] int workbookId, [FromBody] Batch<WorkbookRoleAssignmentSaveInfo, Guid> roleAssignments, CancellationToken cancellationToken)
{
return Ok(await _workbookService.SaveRoleAssignmentsAsync(workbookId, roleAssignments, cancellationToken));
}
}
}
@@ -0,0 +1,7 @@
namespace Strata.Analytics.Api.Models
{
public class EncounterInfo
{
public string EncounterRecordNumber { get; set; }
}
}
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
namespace Strata.Analytics.Models
{
public class PublicationEvent
{
public DateTime RequestDate { get; set; }
public string RequestedBy { get; set; }
public IEnumerable<int> StandardDataSourceIds { get; set; }
public IEnumerable<int> StrataIds { get; set; }
}
}
@@ -0,0 +1,10 @@
namespace Strata.Analytics.Api.Models
{
public class PublishConfig
{
/// <summary>
/// 0 means to publish to all clients
/// </summary>
public int StrataId { get; set; }
}
}
+71
View File
@@ -0,0 +1,71 @@
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using Strata.Configuration.Client.DependencyInjection;
using Strata.Logging.DependencyInjection;
using System;
using System.Diagnostics.CodeAnalysis;
using Strata.Analytics.Biz.DbContexts.Factories;
namespace Strata.Analytics.Api
{
[ExcludeFromCodeCoverage]
public class Program
{
public static void Main(string[] args)
{
try
{
var commandLineApplication = new CommandLineApplication(false);
var doMigrate = commandLineApplication.Option(
"--ef-migrate",
"Apply entity framework migrations and exit",
CommandOptionType.NoValue);
commandLineApplication.HelpOption("-? | -h | --help");
commandLineApplication.OnExecute(() =>
{
ExecuteApp(args, doMigrate);
return 0;
});
commandLineApplication.Execute(args);
}
catch (Exception ex)
{
Log.Error(ex, "There was an unhandled exception that caused the program to crash");
//This is for serilog to ensure that all of the cached logs are flushed when the app crashes
//This is useful so we can capture logs when the application crashes during startup:
Log.CloseAndFlush();
}
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStrataConfiguration()
.UseStrataLogging()
.UseStartup<Startup>();
public static void ExecuteApp(string[] args, CommandOption doMigrate)
{
var webHost = CreateWebHostBuilder(args).Build();
if (doMigrate.HasValue())
{
Log.Debug("Applying Entity Framework migrations");
using var scope = webHost.Services.CreateScope();
using var context = new AnalyticsDbContextFactory().CreateDbContext(new string[] { });
context.Database.Migrate();
Log.Debug("All done, closing app");
Environment.Exit(0);
}
// no flags provided, so just run the webhost
webHost.Run();
}
}
}
@@ -0,0 +1,16 @@
{
"profiles": {
"Strata.Analytics.Api": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"Hangfire__RunLocally": "true",
"ASPNETCORE_ENVIRONMENT": "Development",
"AWS_Profile": "sdt-analytics-service-role",
"AWS_REGION": "us-east-1",
"AWS_S3_US_EAST_1_REGIONAL_ENDPOINT": "regional"
},
"applicationUrl": "https://localhost:8443"
}
}
}
+139
View File
@@ -0,0 +1,139 @@
using Hangfire;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Prometheus;
using Strata.Analytics.Api.Authorization;
using Strata.Analytics.Biz.DataSources.Models;
using Strata.Analytics.Biz.Extracts.Models;
using Strata.Analytics.Biz.Utils;
using Strata.Analytics.Models.Workbooks;
using Strata.ApiLib.Core;
using Strata.ApiLib.Core.Transformers;
using Strata.CoreLib.Claims.Extensions;
using Strata.Hangfire.AspNetCore.Filters.Dashboard;
using Strata.Hangfire.TaskManagement.AspNetCore;
using Strata.SwaggerExtensions;
using System.Diagnostics.CodeAnalysis;
using System.Net.Mime;
using System.Text.Json;
using System.Text.Json.Serialization;
using AuthorizationOptions = Strata.Analytics.Api.Authorization.AuthorizationOptions;
namespace Strata.Analytics.Api
{
[ExcludeFromCodeCoverage]
public class Startup
{
private readonly IConfiguration _configuration;
public Startup(IConfiguration configuration)
{
_configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddStrataPlatformServices(_configuration);
services.AddControllers(options =>
{
//Prevent browsers from caching API responses (this addresses an IE11 issue):
options.Filters.Add(
new ResponseCacheAttribute()
{
Location = ResponseCacheLocation.None,
NoStore = false,
Duration = -1
});
options.ReturnHttpNotAcceptable = true;
options.Filters.Add(new ProducesAttribute(MediaTypeNames.Application.Json));
options.Filters.Add(new ConsumesAttribute(MediaTypeNames.Application.Json));
options.Conventions.Add(new RouteTokenTransformerConvention(new SlugifyParameterTransformer()));
})
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
// Uncomment this line if you want enum to be serialized / deserialized as string value instead of int value
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
// Date and Time Only serialization/deserialization support
options.JsonSerializerOptions.Converters.Add(new DateOnlyJsonConverter());
options.JsonSerializerOptions.Converters.Add(new TimeOnlyJsonConverter());
});
// authorization
services.AddAuthorization(options =>
{
options.AddPolicy(Policies.DataSourceViewer,
policy => policy.Requirements.Add(new DataSourceMinimumRoleRequirement(DataSourceRole.Viewer)));
options.AddPolicy(Policies.DataSourceEditor,
policy => policy.Requirements.Add(new DataSourceMinimumRoleRequirement(DataSourceRole.Editor)));
options.AddPolicy(Policies.WorkbookViewer,
policy => policy.Requirements.Add(new WorkbookMinimumRoleRequirement(WorkbookRole.Viewer)));
options.AddPolicy(Policies.WorkbookEditor,
policy => policy.Requirements.Add(new WorkbookMinimumRoleRequirement(WorkbookRole.Editor)));
options.AddPolicy(Policies.ExtractViewer,
policy => policy.Requirements.Add(new ExtractMinimumRoleRequirement(ExtractRole.Viewer)));
options.AddPolicy(Policies.ExtractEditor,
policy => policy.Requirements.Add(new ExtractMinimumRoleRequirement(ExtractRole.Editor)));
options.AddPolicy(Policies.CustomSqlAccess,
policy => policy.Requirements.Add(new ExtractCustomSqlStrataOnlyRoleRequirement()));
options.AddPolicy(Policies.StrataOnly,
policy => policy.RequireAssertion(context => context.User.IsSdtEmployee()));
options.AddPolicy(Policies.IsAdmin,
policy => policy.RequireAssertion(context => context.User.IsAdmin()));
options.AddPolicy(Policies.AuthorizedDbOnly, policy => policy.Requirements.Add(new AuthorizedDbMinimumRoleRequirement()));
});
services
.AddOptions<AuthorizationOptions>()
.Configure<IConfiguration>((options, configuration) =>
{
configuration.Bind("authorization", options);
})
.ValidateDataAnnotations();
services.AddScoped<IAuthorizationHandler, AuthorizedDbMinimumRoleHandler>();
services.AddScoped<IAuthorizationHandler, DataSourceMinimumRoleHandler>();
services.AddScoped<IAuthorizationHandler, WorkbookMinimumRoleHandler>();
services.AddScoped<IAuthorizationHandler, ExtractMinimumRoleHandler>();
services.AddScoped<IAuthorizationHandler, CustomSQLStrataOnlyRoleHandler>();
// configure hangfire
services.AddStrataHangfire();
services.AddStrataHangfireTaskManagement();
services.AddAnalyticsServices(_configuration);
services.AddRouting();
services.AddSwagger(_configuration);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
app.UseRouting();
app.UseStrataPlatformMiddleware();
app.UseMetricServer();
app.UseHttpMetrics();
app.ConfigureSwagger(env, provider);
app.UseStrataHangfireTaskManagement();
#if DEBUG
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[] { new AllowAnyoneDashboardAuthorizationFilter() }
});
#endif
app.UseEndpoints(endpoints => endpoints.MapControllers().RequireAuthorization());
}
}
}
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
<IncludeOpenAPIAnalyzers>true</IncludeOpenAPIAnalyzers>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.22">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.CommandLineUtils" Version="1.1.1" />
<PackageReference Include="Strata.ApiLib.Core" Version="5.3.0" />
<PackageReference Include="Strata.Configuration.Client" Version="8.26.0" />
<PackageReference Include="Strata.Hangfire.AspNetCore" Version="4.0.0" />
<PackageReference Include="Strata.Hangfire.TaskManagement.AspNetCore" Version="0.6.0" />
<PackageReference Include="Strata.Identity.Client" Version="3.1.0" />
<PackageReference Include="Strata.Logging" Version="3.6.1" />
<PackageReference Include="Strata.SwaggerExtensions" Version="2.3.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Strata.Analytics.Biz\Strata.Analytics.Biz.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,30 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"hangfire": {
"redis": {
"prefix": "analytics-jobs-service",
"configOptions": "ddenredisc01.sdt.local:6379,ddenredisc02.sdt.local:6379,ddenredisc03.sdt.local:6379,ddenredisc04.sdt.local:6379,ddenredisc05.sdt.local:6379,ddenredisc06.sdt.local:6379"
}
},
"endpoints": {
"dataschema": "https://dataschema-api.dev.stratanetwork.net/",
//"dataschema": "https://localhost:9443/",
"systemsettings": "https://settings-api.dev.stratanetwork.net/"
},
"strataApi": {
"clientSecret": "V_okTrkWYBs-PqMP.inbJShIvMGWcpdqCDq_ZjntVF.ydgRCoYaDVJYEWEIdsjJ_"
},
"Snowflake": {
"Url": "https://stratadev.us-east-1.privatelink.snowflakecomputing.com",
"Account": "stratadev"
},
"presignedUrlExpirationInMinutes": 5,
"ServerSideEncryptionS3Key": "arn:aws:kms:us-east-1:809820578507:key/c8f62293-5726-4342-b614-9b23bf2043c9"
}
+17
View File
@@ -0,0 +1,17 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"Version": "0.0.0",
"configuration": {
"basicAuthUsername": "BkQsRDdmdjkFNhKVuTI.TpujQJXuKhGDcV.F_hjADMRTaEUWMJnviaesoOrhhfnz",
"basicAuthPassword": "GWPIIWECDqh.hjDNdpxVEfeFe-bYFwPSx-oBGFD_od_SUQVk-QqTIkZY_NXMeqeo"
},
"StrataConfigServerBaseUrl": "https://configuration.dev.stratanetwork.net",
"authorization": {
"authorizedDbGuid": "45e3b554-86a5-4b43-875d-3da7eed909d8"
}
}