forked from xiongyuxing/tiku-backend.net
feat: enforce tenant isolation and shared question bank
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Tiku.Application.QuestionBanks;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.QuestionBanks;
|
||||
|
||||
public sealed class QuestionReferenceService(
|
||||
TikuDbContext dbContext,
|
||||
IPublicQuestionAccessPolicy accessPolicy,
|
||||
ITenantExecutionScope tenantExecutionScope) : IQuestionReferenceService
|
||||
{
|
||||
public async Task<TenantQuestionReference> ResolveAsync(
|
||||
Guid tenantId,
|
||||
Guid? userId,
|
||||
QuestionLocator locator,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var ownerTenantId = locator.Source switch
|
||||
{
|
||||
QuestionSource.Tenant => await ResolveTenantQuestionAsync(tenantId, locator.QuestionId, cancellationToken),
|
||||
QuestionSource.Platform => await ResolvePlatformQuestionAsync(tenantId, locator.QuestionId, cancellationToken),
|
||||
_ => throw new QuestionLocatorException("question_source_invalid", "Question source is invalid.")
|
||||
};
|
||||
|
||||
var existing = await dbContext.TenantQuestionReferences.SingleOrDefaultAsync(
|
||||
reference =>
|
||||
reference.TenantId == tenantId &&
|
||||
reference.QuestionOwnerTenantId == ownerTenantId &&
|
||||
reference.QuestionId == locator.QuestionId,
|
||||
cancellationToken);
|
||||
if (existing is not null)
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var reference = new TenantQuestionReference
|
||||
{
|
||||
TenantId = tenantId,
|
||||
QuestionOwnerTenantId = ownerTenantId,
|
||||
QuestionId = locator.QuestionId,
|
||||
Source = locator.Source,
|
||||
CreatedBy = userId
|
||||
};
|
||||
dbContext.TenantQuestionReferences.Add(reference);
|
||||
return reference;
|
||||
}
|
||||
|
||||
private async Task<Guid> ResolveTenantQuestionAsync(
|
||||
Guid tenantId,
|
||||
Guid questionId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var exists = await dbContext.Questions.AsNoTracking().AnyAsync(
|
||||
question =>
|
||||
question.TenantId == tenantId &&
|
||||
question.Id == questionId &&
|
||||
question.Status == QuestionStatus.Published,
|
||||
cancellationToken);
|
||||
return exists
|
||||
? tenantId
|
||||
: throw new QuestionLocatorException("question_not_found", "Tenant question was not found.");
|
||||
}
|
||||
|
||||
private async Task<Guid> ResolvePlatformQuestionAsync(
|
||||
Guid tenantId,
|
||||
Guid questionId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await accessPolicy.EnsureCanStartAsync(tenantId, cancellationToken);
|
||||
var ownerTenantId = await tenantExecutionScope.ExecuteAsync(
|
||||
tenantId,
|
||||
"Resolve a platform question for an entitled tenant",
|
||||
async (provider, token) =>
|
||||
{
|
||||
var systemDbContext = provider.GetRequiredService<TikuDbContext>();
|
||||
return await systemDbContext.Questions.AsNoTracking()
|
||||
.Where(question =>
|
||||
question.Id == questionId &&
|
||||
question.Status == QuestionStatus.Published)
|
||||
.Join(
|
||||
systemDbContext.Tenants.AsNoTracking().Where(tenant => tenant.Mode == TenantMode.PlatformOwned),
|
||||
question => question.TenantId,
|
||||
tenant => tenant.Id,
|
||||
(question, tenant) => (Guid?)tenant.Id)
|
||||
.SingleOrDefaultAsync(token);
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
return ownerTenantId ?? throw new QuestionLocatorException(
|
||||
"question_not_found",
|
||||
"Platform question was not found.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user