feat(online-inbox): gate access on Zitadel "user" project role
The Online API now requires the "user" project role (claim urn:zitadel:iam:org:project:roles) instead of an ALLOWED_USER_IDS allowlist. - IOnlineAuthProvider: add GetAccessTokenAsync(forceRefresh) overload - ZitadelAuthProvider: forceRefresh drops the cached token and re-runs the refresh-token grant to mint a fresh, role-bearing token - OnlineInboxApiClient: on 401, force-refresh and retry once; if still 401, throw a clear "missing 'user' role" error - OnlineSyncService: surface the 401 at Error level (no longer silent) - UI: ZitadelTokenInspector decodes the access token after login and warns early when the "user" role is absent (fail-open); shown in settings - docs: online-inbox-api-contract reflects role-based access (no allowlist) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -130,6 +130,52 @@ public sealed class OnlineInboxApiClientTests
|
||||
Assert.Equal(401, ex.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Unauthorized_RetriesOnceWithForcedRefresh_ThenThrowsMissingRole()
|
||||
{
|
||||
var (client, handler) = Build();
|
||||
handler.ResponseStatus = HttpStatusCode.Unauthorized;
|
||||
handler.ResponseBody = "Unauthorized";
|
||||
|
||||
var ex = await Assert.ThrowsAsync<OnlineInboxException>(
|
||||
() => client.PutListsAsync([]));
|
||||
|
||||
Assert.Equal(401, ex.StatusCode);
|
||||
Assert.Equal(OnlineInboxApiClient.MissingRoleMessage, ex.Message);
|
||||
// Original attempt + one forced-refresh retry.
|
||||
Assert.Equal(2, handler.Requests.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Unauthorized_ThenSuccessOnRetry_Succeeds()
|
||||
{
|
||||
var handler = new SequenceHandler(HttpStatusCode.Unauthorized, HttpStatusCode.OK);
|
||||
var http = new HttpClient(handler) { BaseAddress = new Uri("https://inbox.example.com/api/") };
|
||||
var client = new OnlineInboxApiClient(http, new StaticTokenAuthProvider("test-token"));
|
||||
|
||||
await client.PutListsAsync([]);
|
||||
|
||||
Assert.Equal(2, handler.Requests.Count);
|
||||
}
|
||||
|
||||
private sealed class SequenceHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly Queue<HttpStatusCode> _statuses;
|
||||
public List<HttpRequestMessage> Requests { get; } = new();
|
||||
|
||||
public SequenceHandler(params HttpStatusCode[] statuses) => _statuses = new(statuses);
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
|
||||
{
|
||||
Requests.Add(request);
|
||||
var status = _statuses.Count > 0 ? _statuses.Dequeue() : HttpStatusCode.OK;
|
||||
return Task.FromResult(new HttpResponseMessage(status)
|
||||
{
|
||||
Content = new StringContent("[]", Encoding.UTF8, "application/json"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ServerError_Throws_OnlineInboxException_WithStatusCode()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user