1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

[SG-419] Fix problems with push notifications on self-host (#2338)

* Added "internal" to non-user-based request types to avoid failing validation.

* Added handling of unsuccessful response so that JSON parsing eror doesn't occur.

* Added logging for token errors.

(cherry picked from commit dad143b3e42247bc6b397b60803e25d243bd83a5)

* Fixed bug in next auth attempt handling.

* Fixed linting.

* Added deserialization options to handle case insensitivity.

* Added a new method for SendAsync that does not expect a result from the client.

* hasJsonResult param to make Send more reusable

* some cleanup

* fix lint problems

* Added launch config for Notifications.

* Added Notifications to Full Server config.

Co-authored-by: Kyle Spearrin <kyle.spearrin@gmail.com>
This commit is contained in:
Todd Martin 2022-11-01 09:58:28 -04:00 committed by GitHub
parent 14074e1e33
commit e277b9e84e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 109 additions and 23 deletions

50
.vscode/launch.json vendored
View File

@ -40,7 +40,8 @@
"Identity", "Identity",
"Sso", "Sso",
"Icons", "Icons",
"Billing" "Billing",
"Notifications"
], ],
"presentation": { "presentation": {
"hidden": false, "hidden": false,
@ -57,6 +58,7 @@
"EventsProcessor-SelfHost", "EventsProcessor-SelfHost",
"Identity-SelfHost", "Identity-SelfHost",
"Sso-SelfHost", "Sso-SelfHost",
"Notifications-SelfHost"
], ],
"presentation": { "presentation": {
"hidden": false, "hidden": false,
@ -238,6 +240,28 @@
"/Views": "${workspaceFolder}/Views" "/Views": "${workspaceFolder}/Views"
} }
}, },
{
"name": "Notifications",
"presentation": {
"hidden": true,
"group": "cloud",
"order": 100
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"preLaunchTask": "buildNotifications",
"program": "${workspaceFolder}/src/Notifications/bin/Debug/net6.0/Notifications.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Notifications",
"stopAtEntry": false,
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{ {
"name": "Identity-SelfHost", "name": "Identity-SelfHost",
"presentation": { "presentation": {
@ -336,6 +360,30 @@
"/Views": "${workspaceFolder}/Views" "/Views": "${workspaceFolder}/Views"
} }
}, },
{
"name": "Notifications-SelfHost",
"presentation": {
"hidden": true,
"group": "self-host",
"order": 999
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"preLaunchTask": "buildNotifications",
"program": "${workspaceFolder}/src/Notifications/bin/Debug/net6.0/Notifications.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Notifications",
"stopAtEntry": false,
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "http://localhost:61841",
"developSelfHosted": "true"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{ {
"name": "EventsProcessor-SelfHost", "name": "EventsProcessor-SelfHost",
"presentation": { "presentation": {

16
.vscode/tasks.json vendored
View File

@ -89,6 +89,22 @@
"isDefault": true "isDefault": true
} }
}, },
{
"label": "buildNotifications",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/src/Notifications/Notifications.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile",
"group": {
"kind": "build",
"isDefault": true
}
},
{ {
"label": "buildBilling", "label": "buildBilling",
"command": "dotnet", "command": "dotnet",

View File

@ -72,12 +72,13 @@ public class SelfHostedSyncSponsorshipsCommand : BaseIdentityClientService, ISel
foreach (var orgSponsorshipsBatch in organizationSponsorshipsDict.Values.Chunk(1000)) foreach (var orgSponsorshipsBatch in organizationSponsorshipsDict.Values.Chunk(1000))
{ {
var response = await SendAsync<OrganizationSponsorshipSyncRequestModel, OrganizationSponsorshipSyncResponseModel>(HttpMethod.Post, "organization/sponsorship/sync", new OrganizationSponsorshipSyncRequestModel var response = await SendAsync<OrganizationSponsorshipSyncRequestModel, OrganizationSponsorshipSyncResponseModel>(
{ HttpMethod.Post, "organization/sponsorship/sync", new OrganizationSponsorshipSyncRequestModel
BillingSyncKey = billingSyncConfig.BillingSyncKey, {
SponsoringOrganizationCloudId = cloudOrganizationId, BillingSyncKey = billingSyncConfig.BillingSyncKey,
SponsorshipsBatch = orgSponsorshipsBatch.Select(s => new OrganizationSponsorshipRequestModel(s)) SponsoringOrganizationCloudId = cloudOrganizationId,
}); SponsorshipsBatch = orgSponsorshipsBatch.Select(s => new OrganizationSponsorshipRequestModel(s))
}, true);
if (response == null) if (response == null)
{ {

View File

@ -47,19 +47,21 @@ public abstract class BaseIdentityClientService : IDisposable
protected string AccessToken { get; private set; } protected string AccessToken { get; private set; }
protected Task SendAsync(HttpMethod method, string path) => protected Task SendAsync(HttpMethod method, string path) =>
SendAsync<object, object>(method, path, null); SendAsync<object>(method, path, null);
protected Task SendAsync<TRequest>(HttpMethod method, string path, TRequest body) => protected Task SendAsync<TRequest>(HttpMethod method, string path, TRequest requestModel) =>
SendAsync<TRequest, object>(method, path, body); SendAsync<TRequest, object>(method, path, requestModel, false);
protected async Task<TResult> SendAsync<TRequest, TResult>(HttpMethod method, string path, TRequest requestModel) protected async Task<TResult> SendAsync<TRequest, TResult>(HttpMethod method, string path,
TRequest requestModel, bool hasJsonResult)
{ {
var fullRequestPath = string.Concat(Client.BaseAddress, path); var fullRequestPath = string.Concat(Client.BaseAddress, path);
var tokenStateResponse = await HandleTokenStateAsync(); var tokenStateResponse = await HandleTokenStateAsync();
if (!tokenStateResponse) if (!tokenStateResponse)
{ {
_logger.LogError("Unable to send {method} request to {requestUri} because an access token was unable to be obtained", method.Method, fullRequestPath); _logger.LogError("Unable to send {method} request to {requestUri} because an access token was unable to be obtained",
method.Method, fullRequestPath);
return default; return default;
} }
@ -71,7 +73,19 @@ public abstract class BaseIdentityClientService : IDisposable
try try
{ {
var response = await Client.SendAsync(message); var response = await Client.SendAsync(message);
return await response.Content.ReadFromJsonAsync<TResult>(); if (response.IsSuccessStatusCode)
{
if (hasJsonResult)
{
return await response.Content.ReadFromJsonAsync<TResult>();
}
}
else
{
_logger.LogError("Request to {url} is unsuccessful with status of {code}-{reason}",
message.RequestUri.ToString(), response.StatusCode, response.ReasonPhrase);
}
return default;
} }
catch (Exception e) catch (Exception e)
{ {
@ -82,8 +96,9 @@ public abstract class BaseIdentityClientService : IDisposable
protected async Task<bool> HandleTokenStateAsync() protected async Task<bool> HandleTokenStateAsync()
{ {
if (_nextAuthAttempt.HasValue && DateTime.UtcNow > _nextAuthAttempt.Value) if (_nextAuthAttempt.HasValue && DateTime.UtcNow < _nextAuthAttempt.Value)
{ {
_logger.LogInformation("Not requesting a token at {now} because the next request time is {nextAttempt}", DateTime.UtcNow, _nextAuthAttempt.Value);
return false; return false;
} }
_nextAuthAttempt = null; _nextAuthAttempt = null;
@ -118,12 +133,13 @@ public abstract class BaseIdentityClientService : IDisposable
if (response == null) if (response == null)
{ {
_logger.LogError("Empty token response from {identity} for client {clientId} with status {code}-{reason}", IdentityClient.BaseAddress, _identityClientId, response.StatusCode, response.ReasonPhrase);
return false; return false;
} }
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
_logger.LogInformation("Unsuccessful token response from {identity} for client {clientId} with status code {StatusCode}", IdentityClient.BaseAddress, _identityClientId, response.StatusCode); _logger.LogError("Unsuccessful token response from {identity} for client {clientId} with status {code}-{reason}", IdentityClient.BaseAddress, _identityClientId, response.StatusCode, response.ReasonPhrase);
if (response.StatusCode == HttpStatusCode.BadRequest) if (response.StatusCode == HttpStatusCode.BadRequest)
{ {
@ -139,7 +155,8 @@ public abstract class BaseIdentityClientService : IDisposable
return false; return false;
} }
using var jsonDocument = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); var content = await response.Content.ReadAsStreamAsync();
using var jsonDocument = await JsonDocument.ParseAsync(content);
AccessToken = jsonDocument.RootElement.GetProperty("access_token").GetString(); AccessToken = jsonDocument.RootElement.GetProperty("access_token").GetString();
return true; return true;

View File

@ -52,7 +52,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
string[] allowedGrantTypes = { "authorization_code", "client_credentials" }; string[] allowedGrantTypes = { "authorization_code", "client_credentials" };
if (!allowedGrantTypes.Contains(context.Result.ValidatedRequest.GrantType) if (!allowedGrantTypes.Contains(context.Result.ValidatedRequest.GrantType)
|| context.Result.ValidatedRequest.ClientId.StartsWith("organization") || context.Result.ValidatedRequest.ClientId.StartsWith("organization")
|| context.Result.ValidatedRequest.ClientId.StartsWith("installation")) || context.Result.ValidatedRequest.ClientId.StartsWith("installation")
|| context.Result.ValidatedRequest.ClientId.StartsWith("internal"))
{ {
return; return;
} }

View File

@ -7,6 +7,9 @@ namespace Bit.Notifications;
public static class HubHelpers public static class HubHelpers
{ {
private static JsonSerializerOptions _deserializerOptions =
new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
public static async Task SendNotificationToHubAsync( public static async Task SendNotificationToHubAsync(
string notificationJson, string notificationJson,
IHubContext<NotificationsHub> hubContext, IHubContext<NotificationsHub> hubContext,
@ -23,7 +26,7 @@ public static class HubHelpers
case PushType.SyncLoginDelete: case PushType.SyncLoginDelete:
var cipherNotification = var cipherNotification =
JsonSerializer.Deserialize<PushNotificationData<SyncCipherPushNotification>>( JsonSerializer.Deserialize<PushNotificationData<SyncCipherPushNotification>>(
notificationJson); notificationJson, _deserializerOptions);
if (cipherNotification.Payload.UserId.HasValue) if (cipherNotification.Payload.UserId.HasValue)
{ {
await hubContext.Clients.User(cipherNotification.Payload.UserId.ToString()) await hubContext.Clients.User(cipherNotification.Payload.UserId.ToString())
@ -41,7 +44,7 @@ public static class HubHelpers
case PushType.SyncFolderDelete: case PushType.SyncFolderDelete:
var folderNotification = var folderNotification =
JsonSerializer.Deserialize<PushNotificationData<SyncFolderPushNotification>>( JsonSerializer.Deserialize<PushNotificationData<SyncFolderPushNotification>>(
notificationJson); notificationJson, _deserializerOptions);
await hubContext.Clients.User(folderNotification.Payload.UserId.ToString()) await hubContext.Clients.User(folderNotification.Payload.UserId.ToString())
.SendAsync("ReceiveMessage", folderNotification, cancellationToken); .SendAsync("ReceiveMessage", folderNotification, cancellationToken);
break; break;
@ -52,7 +55,7 @@ public static class HubHelpers
case PushType.LogOut: case PushType.LogOut:
var userNotification = var userNotification =
JsonSerializer.Deserialize<PushNotificationData<UserPushNotification>>( JsonSerializer.Deserialize<PushNotificationData<UserPushNotification>>(
notificationJson); notificationJson, _deserializerOptions);
await hubContext.Clients.User(userNotification.Payload.UserId.ToString()) await hubContext.Clients.User(userNotification.Payload.UserId.ToString())
.SendAsync("ReceiveMessage", userNotification, cancellationToken); .SendAsync("ReceiveMessage", userNotification, cancellationToken);
break; break;
@ -61,21 +64,21 @@ public static class HubHelpers
case PushType.SyncSendDelete: case PushType.SyncSendDelete:
var sendNotification = var sendNotification =
JsonSerializer.Deserialize<PushNotificationData<SyncSendPushNotification>>( JsonSerializer.Deserialize<PushNotificationData<SyncSendPushNotification>>(
notificationJson); notificationJson, _deserializerOptions);
await hubContext.Clients.User(sendNotification.Payload.UserId.ToString()) await hubContext.Clients.User(sendNotification.Payload.UserId.ToString())
.SendAsync("ReceiveMessage", sendNotification, cancellationToken); .SendAsync("ReceiveMessage", sendNotification, cancellationToken);
break; break;
case PushType.AuthRequestResponse: case PushType.AuthRequestResponse:
var authRequestResponseNotification = var authRequestResponseNotification =
JsonSerializer.Deserialize<PushNotificationData<AuthRequestPushNotification>>( JsonSerializer.Deserialize<PushNotificationData<AuthRequestPushNotification>>(
notificationJson); notificationJson, _deserializerOptions);
await anonymousHubContext.Clients.Group(authRequestResponseNotification.Payload.Id.ToString()) await anonymousHubContext.Clients.Group(authRequestResponseNotification.Payload.Id.ToString())
.SendAsync("AuthRequestResponseRecieved", authRequestResponseNotification, cancellationToken); .SendAsync("AuthRequestResponseRecieved", authRequestResponseNotification, cancellationToken);
break; break;
case PushType.AuthRequest: case PushType.AuthRequest:
var authRequestNotification = var authRequestNotification =
JsonSerializer.Deserialize<PushNotificationData<AuthRequestPushNotification>>( JsonSerializer.Deserialize<PushNotificationData<AuthRequestPushNotification>>(
notificationJson); notificationJson, _deserializerOptions);
await hubContext.Clients.User(authRequestNotification.Payload.UserId.ToString()) await hubContext.Clients.User(authRequestNotification.Payload.UserId.ToString())
.SendAsync("ReceiveMessage", authRequestNotification, cancellationToken); .SendAsync("ReceiveMessage", authRequestNotification, cancellationToken);
break; break;