diff --git a/Notesnook.API/Authorization/SyncRequirement.cs b/Notesnook.API/Authorization/SyncRequirement.cs index bf60532..eb249f2 100644 --- a/Notesnook.API/Authorization/SyncRequirement.cs +++ b/Notesnook.API/Authorization/SyncRequirement.cs @@ -1,102 +1,102 @@ -/* -This file is part of the Notesnook Sync Server project (https://notesnook.com/) - -Copyright (C) 2023 Streetwriters (Private) Limited - -This program is free software: you can redistribute it and/or modify -it under the terms of the Affero GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -Affero GNU General Public License for more details. - -You should have received a copy of the Affero GNU General Public License -along with this program. If not, see . -*/ - -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Authorization.Policy; -using Microsoft.AspNetCore.Http; - -namespace Notesnook.API.Authorization -{ - public class SyncRequirement : AuthorizationHandler, IAuthorizationRequirement - { - private readonly Dictionary pathErrorPhraseMap = new() - { - ["/sync/attachments"] = "use attachments", - ["/sync"] = "sync your notes", - ["/hubs/sync"] = "sync your notes", - ["/hubs/sync/v2"] = "sync your notes", - ["/monographs"] = "publish monographs" - }; - - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SyncRequirement requirement) - { - PathString path = context.Resource is DefaultHttpContext httpContext ? httpContext.Request.Path : null; - var result = this.IsAuthorized(context.User, path); - if (result.Succeeded) context.Succeed(requirement); - else if (result.AuthorizationFailure.FailureReasons.Any()) - context.Fail(result.AuthorizationFailure.FailureReasons.First()); - else context.Fail(); - - return Task.CompletedTask; - } - - public PolicyAuthorizationResult IsAuthorized(ClaimsPrincipal User, PathString requestPath) - { - var id = User.FindFirstValue("sub"); - - if (string.IsNullOrEmpty(id)) - { - var reason = new[] - { - new AuthorizationFailureReason(this, "Invalid token.") - }; - return PolicyAuthorizationResult.Forbid(AuthorizationFailure.Failed(reason)); - } - - var hasSyncScope = User.HasClaim("scope", "notesnook.sync"); - var isInAudience = User.HasClaim("aud", "notesnook"); - var hasRole = User.HasClaim("role", "notesnook"); - - var isEmailVerified = User.HasClaim("verified", "true"); - - if (!isEmailVerified) - { - var phrase = "continue"; - - foreach (var item in pathErrorPhraseMap) - { - if (requestPath != null && requestPath.StartsWithSegments(item.Key)) - phrase = item.Value; - } - - var error = $"Please confirm your email to {phrase}."; - var reason = new[] - { - new AuthorizationFailureReason(this, error) - }; - return PolicyAuthorizationResult.Forbid(AuthorizationFailure.Failed(reason)); - // context.Fail(new AuthorizationFailureReason(this, error)); - } - - if (hasSyncScope && isInAudience && hasRole && isEmailVerified) - return PolicyAuthorizationResult.Success(); //(requirement); - return PolicyAuthorizationResult.Forbid(); - } - - public override Task HandleAsync(AuthorizationHandlerContext context) - { - return this.HandleRequirementAsync(context, this); - } - - } +/* +This file is part of the Notesnook Sync Server project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the Affero GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +Affero GNU General Public License for more details. + +You should have received a copy of the Affero GNU General Public License +along with this program. If not, see . +*/ + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Policy; +using Microsoft.AspNetCore.Http; + +namespace Notesnook.API.Authorization +{ + public class SyncRequirement : AuthorizationHandler, IAuthorizationRequirement + { + private readonly Dictionary pathErrorPhraseMap = new() + { + ["/sync/attachments"] = "use attachments", + ["/sync"] = "sync your notes", + ["/hubs/sync"] = "sync your notes", + ["/hubs/sync/v2"] = "sync your notes", + ["/monographs"] = "publish monographs" + }; + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SyncRequirement requirement) + { + PathString path = context.Resource is DefaultHttpContext httpContext ? httpContext.Request.Path : null; + var result = this.IsAuthorized(context.User, path); + if (result.Succeeded) context.Succeed(requirement); + else if (result.AuthorizationFailure.FailureReasons.Any()) + context.Fail(result.AuthorizationFailure.FailureReasons.First()); + else context.Fail(); + + return Task.CompletedTask; + } + + public PolicyAuthorizationResult IsAuthorized(ClaimsPrincipal? User, PathString requestPath) + { + var id = User?.FindFirstValue("sub"); + + if (string.IsNullOrEmpty(id)) + { + var reason = new[] + { + new AuthorizationFailureReason(this, "Invalid token.") + }; + return PolicyAuthorizationResult.Forbid(AuthorizationFailure.Failed(reason)); + } + + var hasSyncScope = User.HasClaim("scope", "notesnook.sync"); + var isInAudience = User.HasClaim("aud", "notesnook"); + var hasRole = User.HasClaim("role", "notesnook"); + + var isEmailVerified = User.HasClaim("verified", "true"); + + if (!isEmailVerified) + { + var phrase = "continue"; + + foreach (var item in pathErrorPhraseMap) + { + if (requestPath != null && requestPath.StartsWithSegments(item.Key)) + phrase = item.Value; + } + + var error = $"Please confirm your email to {phrase}."; + var reason = new[] + { + new AuthorizationFailureReason(this, error) + }; + return PolicyAuthorizationResult.Forbid(AuthorizationFailure.Failed(reason)); + // context.Fail(new AuthorizationFailureReason(this, error)); + } + + if (hasSyncScope && isInAudience && hasRole && isEmailVerified) + return PolicyAuthorizationResult.Success(); //(requirement); + return PolicyAuthorizationResult.Forbid(); + } + + public override Task HandleAsync(AuthorizationHandlerContext context) + { + return this.HandleRequirementAsync(context, this); + } + + } } \ No newline at end of file diff --git a/Notesnook.API/Hubs/SyncV2Hub.cs b/Notesnook.API/Hubs/SyncV2Hub.cs index 2609d33..27390cd 100644 --- a/Notesnook.API/Hubs/SyncV2Hub.cs +++ b/Notesnook.API/Hubs/SyncV2Hub.cs @@ -74,10 +74,10 @@ namespace Notesnook.API.Hubs var result = new SyncRequirement().IsAuthorized(Context.User, new PathString("/hubs/sync/v2")); if (!result.Succeeded) { - var reason = result.AuthorizationFailure.FailureReasons.FirstOrDefault(); + var reason = result.AuthorizationFailure?.FailureReasons.FirstOrDefault(); throw new HubException(reason?.Message ?? "Unauthorized"); } - var id = Context.User.FindFirstValue("sub"); + var id = Context.User?.FindFirstValue("sub") ?? throw new HubException("User not found."); await Groups.AddToGroupAsync(Context.ConnectionId, id); await base.OnConnectedAsync(); } @@ -103,13 +103,11 @@ namespace Notesnook.API.Hubs public async Task PushItems(string deviceId, SyncTransferItemV2 pushItem) { - var userId = Context.User.FindFirstValue("sub"); - if (string.IsNullOrEmpty(userId)) throw new HubException("Please login to sync."); + var userId = Context.User?.FindFirstValue("sub") ?? throw new HubException("Please login to sync."); SyncEventCounterSource.Log.PushV2(); - var stopwatch = new Stopwatch(); - stopwatch.Start(); + var stopwatch = Stopwatch.StartNew(); try { @@ -123,14 +121,13 @@ namespace Notesnook.API.Hubs } finally { - stopwatch.Stop(); SyncEventCounterSource.Log.RecordPushDuration(stopwatch.ElapsedMilliseconds); } } public async Task PushCompleted() { - var userId = Context.User.FindFirstValue("sub"); + var userId = Context.User?.FindFirstValue("sub") ?? throw new HubException("User not found."); await Clients.OthersInGroup(userId).PushCompleted(); return true; } @@ -197,8 +194,7 @@ namespace Notesnook.API.Hubs private async Task HandleRequestFetch(string deviceId, bool includeMonographs) { - var userId = Context.User.FindFirstValue("sub"); - if (string.IsNullOrEmpty(userId)) throw new HubException("Please login to sync."); + var userId = Context.User?.FindFirstValue("sub") ?? throw new HubException("Please login to sync."); SyncEventCounterSource.Log.FetchV2(); @@ -214,8 +210,7 @@ namespace Notesnook.API.Hubs !isResetSync) return new SyncV2Metadata { Synced = true }; - var stopwatch = new Stopwatch(); - stopwatch.Start(); + var stopwatch = Stopwatch.StartNew(); try { string[] ids = deviceService.FetchUnsyncedIds(); @@ -285,7 +280,6 @@ namespace Notesnook.API.Hubs } finally { - stopwatch.Stop(); SyncEventCounterSource.Log.RecordFetchDuration(stopwatch.ElapsedMilliseconds); } } diff --git a/Notesnook.API/Interfaces/IS3Service.cs b/Notesnook.API/Interfaces/IS3Service.cs index 509d9c3..6c83d38 100644 --- a/Notesnook.API/Interfaces/IS3Service.cs +++ b/Notesnook.API/Interfaces/IS3Service.cs @@ -1,40 +1,40 @@ -/* -This file is part of the Notesnook Sync Server project (https://notesnook.com/) - -Copyright (C) 2023 Streetwriters (Private) Limited - -This program is free software: you can redistribute it and/or modify -it under the terms of the Affero GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -Affero GNU General Public License for more details. - -You should have received a copy of the Affero GNU General Public License -along with this program. If not, see . -*/ - -using System.Threading; -using System.Threading.Tasks; -using Amazon.S3.Model; -using Notesnook.API.Models; -using Notesnook.API.Models.Responses; -using Streetwriters.Common.Interfaces; - -namespace Notesnook.API.Interfaces -{ - public interface IS3Service - { - Task DeleteObjectAsync(string userId, string name); - Task DeleteDirectoryAsync(string userId); - Task GetObjectSizeAsync(string userId, string name); - string GetUploadObjectUrl(string userId, string name); - string GetDownloadObjectUrl(string userId, string name); - Task StartMultipartUploadAsync(string userId, string name, int parts, string uploadId = null); - Task AbortMultipartUploadAsync(string userId, string name, string uploadId); - Task CompleteMultipartUploadAsync(string userId, CompleteMultipartUploadRequest uploadRequest); - } +/* +This file is part of the Notesnook Sync Server project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the Affero GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +Affero GNU General Public License for more details. + +You should have received a copy of the Affero GNU General Public License +along with this program. If not, see . +*/ + +using System.Threading; +using System.Threading.Tasks; +using Amazon.S3.Model; +using Notesnook.API.Models; +using Notesnook.API.Models.Responses; +using Streetwriters.Common.Interfaces; + +namespace Notesnook.API.Interfaces +{ + public interface IS3Service + { + Task DeleteObjectAsync(string userId, string name); + Task DeleteDirectoryAsync(string userId); + Task GetObjectSizeAsync(string userId, string name); + string? GetUploadObjectUrl(string userId, string name); + string GetDownloadObjectUrl(string userId, string name); + Task StartMultipartUploadAsync(string userId, string name, int parts, string uploadId = null); + Task AbortMultipartUploadAsync(string userId, string name, string uploadId); + Task CompleteMultipartUploadAsync(string userId, CompleteMultipartUploadRequest uploadRequest); + } } \ No newline at end of file diff --git a/Notesnook.API/Services/S3Service.cs b/Notesnook.API/Services/S3Service.cs index e6afa3f..41a0e4f 100644 --- a/Notesnook.API/Services/S3Service.cs +++ b/Notesnook.API/Services/S3Service.cs @@ -155,11 +155,9 @@ namespace Notesnook.API.Services } - public string GetUploadObjectUrl(string userId, string name) + public string? GetUploadObjectUrl(string userId, string name) { - var url = this.GetPresignedURL(userId, name, HttpVerb.PUT); - if (url == null) return null; - return url; + return this.GetPresignedURL(userId, name, HttpVerb.PUT); } public string GetDownloadObjectUrl(string userId, string name) @@ -215,7 +213,7 @@ namespace Notesnook.API.Services if (!IsSuccessStatusCode(((int)response.HttpStatusCode))) throw new Exception("Failed to complete multipart upload."); } - private string GetPresignedURL(string userId, string name, HttpVerb httpVerb, S3ClientMode mode = S3ClientMode.EXTERNAL) + private string? GetPresignedURL(string userId, string name, HttpVerb httpVerb, S3ClientMode mode = S3ClientMode.EXTERNAL) { var objectName = GetFullObjectName(userId, name); if (userId == null || objectName == null) return null;