refactor!: rename the project from Monitor to MoShiro [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful
ci/woodpecker/push/main-amd64-build Pipeline was successful
Namespaces, assemblies, solution, binaries, images, systemd units, config paths, environment variables, protocol methods and the Linux user are all renamed. Breaking: existing databases, node identities and installed units must be recreated.
This commit is contained in:
1 parent
27ac7fb787
commit
2e146d4da7
309 files changed
+1565
-1572
No files matched your search
@@ -0,0 +1,106 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MoShiro.Backend.Application;
|
||||
using MoShiro.Protocol;
|
||||
|
||||
namespace MoShiro.Backend.Api;
|
||||
|
||||
public static class HttpContextPrincipalExtensions
|
||||
{
|
||||
private const string Key = "moshiro.principal";
|
||||
|
||||
public static Principal GetPrincipal(this HttpContext http) =>
|
||||
http.Items.TryGetValue(Key, out object? value) && value is Principal principal ? principal : Principal.Anonymous;
|
||||
|
||||
public static void SetPrincipal(this HttpContext http, Principal principal) => http.Items[Key] = principal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 认证:<c>Authorization: Bearer <api_token></c> 或会话 Cookie。作为中间件运行,只解析身份;授权由各层的守卫决定。
|
||||
/// </summary>
|
||||
public static class PrincipalResolver
|
||||
{
|
||||
public const string SessionCookie = "moshiro_session";
|
||||
public const string CsrfHeader = "X-CSRF-Token";
|
||||
|
||||
public static string? BearerOf(HttpContext http)
|
||||
{
|
||||
string? header = http.Request.Headers.Authorization;
|
||||
return header is not null && header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) ? header["Bearer ".Length..].Trim() : null;
|
||||
}
|
||||
|
||||
public static async Task ResolveAsync(HttpContext http, Func<Task> next)
|
||||
{
|
||||
AuthService auth = http.RequestServices.GetRequiredService<AuthService>();
|
||||
Principal? principal = null;
|
||||
string? bearer = BearerOf(http);
|
||||
if (bearer is not null)
|
||||
{
|
||||
principal = await auth.AuthenticateTokenAsync(bearer, http.RequestAborted);
|
||||
if (principal is null && await auth.AuthenticateSessionAsync(bearer, http.RequestAborted) is { } viaSession)
|
||||
{
|
||||
// 登录时用 return_session 拿到的会话令牌:和 API 令牌一样走 Bearer,不是 Cookie,所以不需要 CSRF
|
||||
principal = viaSession with { Kind = PrincipalKind.Token, CsrfToken = null };
|
||||
}
|
||||
}
|
||||
else if (http.Request.Cookies.TryGetValue(SessionCookie, out string? session) && !string.IsNullOrEmpty(session))
|
||||
{
|
||||
principal = await auth.AuthenticateSessionAsync(session, http.RequestAborted);
|
||||
}
|
||||
|
||||
http.SetPrincipal(principal ?? Principal.Anonymous);
|
||||
await next();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>管理层守卫:需要 admin 作用域;Cookie 会话的非 GET 请求必须带 <c>X-CSRF-Token</c>。</summary>
|
||||
public static class AdminGuard
|
||||
{
|
||||
public static async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
|
||||
{
|
||||
HttpContext http = context.HttpContext;
|
||||
Principal principal = http.GetPrincipal();
|
||||
if (!principal.IsAdmin)
|
||||
{
|
||||
return principal.Scope != ApiScope.None
|
||||
? Problems.Create(http, MoShiroErrors.Forbidden, "the token scope does not allow this operation")
|
||||
: Problems.Create(http, MoShiroErrors.Unauthorized, "authentication is required");
|
||||
}
|
||||
|
||||
if (principal.Kind == PrincipalKind.Session && !HttpMethods.IsGet(http.Request.Method) && !HttpMethods.IsHead(http.Request.Method) && !HttpMethods.IsOptions(http.Request.Method))
|
||||
{
|
||||
string? csrf = http.Request.Headers[PrincipalResolver.CsrfHeader];
|
||||
if (csrf is null || principal.CsrfToken is null || !AuthService.FixedTimeEquals(csrf, principal.CsrfToken))
|
||||
{
|
||||
return Problems.Create(http, MoShiroErrors.CsrfFailed, "a valid X-CSRF-Token header is required");
|
||||
}
|
||||
}
|
||||
|
||||
return await next(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>登记发布清单的守卫:管理员(会话要带 CSRF)或 release 令牌;release 令牌只能做这一件事。</summary>
|
||||
public static class ReleaseGuard
|
||||
{
|
||||
public static async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) =>
|
||||
context.HttpContext.GetPrincipal().Scope == ApiScope.Release ? await next(context) : await AdminGuard.InvokeAsync(context, next);
|
||||
}
|
||||
|
||||
/// <summary>展示层守卫:站点公开时匿名可读;私有时需要登录或只读令牌。</summary>
|
||||
public static class PublicGuard
|
||||
{
|
||||
public static async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
|
||||
{
|
||||
HttpContext http = context.HttpContext;
|
||||
if (!http.GetPrincipal().CanRead && !await http.RequestServices.GetRequiredService<SettingsService>().IsSitePublicAsync(http.RequestAborted))
|
||||
{
|
||||
return Problems.Create(http, MoShiroErrors.SitePrivate, "this site is private; sign in or use a read-only token");
|
||||
}
|
||||
|
||||
return await next(context);
|
||||
}
|
||||
|
||||
/// <summary>被隐藏(public_visible = false)的线路只对已认证的访问者可见。</summary>
|
||||
public static bool CanSeeHidden(HttpContext http) => http.GetPrincipal().CanRead;
|
||||
}
|
||||
Reference in new issue
Block a user