feat(security): add distributed authorization foundation

This commit is contained in:
2026-07-29 10:40:10 +08:00
parent c7f9a4e3c9
commit df88fa19cb
76 changed files with 22020 additions and 88 deletions

View File

@@ -404,6 +404,10 @@ public sealed class AuthService(
request.TenantId.Value,
user.Id,
cancellationToken);
// Persist the external identity and membership together only after the
// tenant policy and existing membership state have accepted the login.
// A denied first login must not leave a user or provider identity behind.
await dbContext.SaveChangesAsync(cancellationToken);
return await CompleteSuccessfulLoginAsync(
request.Realm,
@@ -502,7 +506,6 @@ public sealed class AuthService(
existingIdentity.UserId = user.Id;
existingIdentity.OpenId = wechatIdentity.OpenId;
existingIdentity.UnionId = wechatIdentity.UnionId;
await dbContext.SaveChangesAsync(cancellationToken);
return user;
}
@@ -552,22 +555,26 @@ public sealed class AuthService(
membership.UserId == userId &&
membership.Role == TenantRole.Student,
cancellationToken);
if (studentMembership is null)
if (studentMembership is not null)
{
dbContext.TenantMemberships.Add(new TenantMembership
{
TenantId = tenantId,
UserId = userId,
Role = TenantRole.Student,
Status = MembershipStatus.Active
});
}
else
{
studentMembership.Status = MembershipStatus.Active;
// Invited and Disabled memberships require an explicit administrator action.
throw new TenantAccessDeniedException();
}
await dbContext.SaveChangesAsync(cancellationToken);
var policy = await dbContext.TenantAuthPolicies.AsNoTracking()
.SingleOrDefaultAsync(item => item.TenantId == tenantId, cancellationToken);
if (policy is not null && !policy.AllowExternalStudentSelfRegistration)
{
throw new TenantAccessDeniedException();
}
dbContext.TenantMemberships.Add(new TenantMembership
{
TenantId = tenantId,
UserId = userId,
Role = TenantRole.Student,
Status = MembershipStatus.Active
});
}
private async Task<TenantMembership?> FindActiveMembershipAsync(