From d1421d640fcb8030a914dcf4e1507607f1c7ccba Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Mon, 13 Oct 2025 11:27:07 +0500 Subject: [PATCH] identity: fix user subscription claim value incorrect for legacy pro users --- .../Controllers/MonographsController.cs | 38 +++++++------ .../Extensions/ClaimsPrincipalExtensions.cs | 2 +- .../Interfaces/IUserSubscriptionService.cs | 1 + .../Controllers/SignupController.cs | 2 +- .../MessageHandlers/CreateSubscription.cs | 13 ++--- .../MessageHandlers/CreateSubscriptionV2.cs | 49 ---------------- .../Services/UserService.cs | 56 ++++--------------- Streetwriters.Identity/Startup.cs | 14 +---- 8 files changed, 44 insertions(+), 131 deletions(-) delete mode 100644 Streetwriters.Identity/MessageHandlers/CreateSubscriptionV2.cs diff --git a/Notesnook.API/Controllers/MonographsController.cs b/Notesnook.API/Controllers/MonographsController.cs index 9b2a6cf..7a0e38d 100644 --- a/Notesnook.API/Controllers/MonographsController.cs +++ b/Notesnook.API/Controllers/MonographsController.cs @@ -105,7 +105,7 @@ namespace Notesnook.API.Controllers if (existingMonograph != null && !existingMonograph.Deleted) return await UpdateAsync(deviceId, monograph); if (monograph.EncryptedContent == null) - monograph.CompressedContent = (await CleanupContentAsync(monograph.Content)).CompressBrotli(); + monograph.CompressedContent = (await CleanupContentAsync(User, monograph.Content)).CompressBrotli(); monograph.UserId = userId; monograph.DatePublished = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); @@ -157,7 +157,7 @@ namespace Notesnook.API.Controllers return base.BadRequest("Monograph is too big. Max allowed size is 15mb."); if (monograph.EncryptedContent == null) - monograph.CompressedContent = (await CleanupContentAsync(monograph.Content)).CompressBrotli(); + monograph.CompressedContent = (await CleanupContentAsync(User, monograph.Content)).CompressBrotli(); else monograph.Content = null; @@ -309,25 +309,15 @@ namespace Notesnook.API.Controllers }); } - private async Task CleanupContentAsync(string content) + private async Task CleanupContentAsync(ClaimsPrincipal user, string content) { + if (Constants.IS_SELF_HOSTED) return content; try { var json = JsonSerializer.Deserialize(content); var html = json.Data; - if (!Constants.IS_SELF_HOSTED && !User.IsUserSubscribed()) - { - var config = Configuration.Default.WithDefaultLoader(); - var context = BrowsingContext.New(config); - var document = await context.OpenAsync(r => r.Content(html)); - foreach (var element in document.QuerySelectorAll("a,iframe,img,object,svg,button,link")) - { - element.Remove(); - } - html = document.ToHtml(); - } - if (User.IsUserSubscribed()) + if (user.IsUserSubscribed()) { var config = Configuration.Default.WithDefaultLoader(); var context = BrowsingContext.New(config); @@ -336,7 +326,23 @@ namespace Notesnook.API.Controllers { var href = element.GetAttribute("href"); if (string.IsNullOrEmpty(href)) continue; - if (!await analyzer.IsURLSafeAsync(href)) element.RemoveAttribute("href"); + if (!await analyzer.IsURLSafeAsync(href)) + { + await Slogger.Info("CleanupContentAsync", "Malicious URL detected: " + href); + element.RemoveAttribute("href"); + } + } + html = document.ToHtml(); + } + else + { + var config = Configuration.Default.WithDefaultLoader(); + var context = BrowsingContext.New(config); + var document = await context.OpenAsync(r => r.Content(html)); + foreach (var element in document.QuerySelectorAll("a,iframe,img,object,svg,button,link")) + { + foreach (var attr in element.Attributes) + element.RemoveAttribute(attr.Name); } html = document.ToHtml(); } diff --git a/Notesnook.API/Extensions/ClaimsPrincipalExtensions.cs b/Notesnook.API/Extensions/ClaimsPrincipalExtensions.cs index c57dd30..0cb0bec 100644 --- a/Notesnook.API/Extensions/ClaimsPrincipalExtensions.cs +++ b/Notesnook.API/Extensions/ClaimsPrincipalExtensions.cs @@ -7,7 +7,7 @@ namespace System.Security.Claims { public static class ClaimsPrincipalExtensions { - private readonly static string[] SUBSCRIBED_CLAIMS = ["believer", "education", "essential", "pro", "premium", "premium_canceled"]; + private readonly static string[] SUBSCRIBED_CLAIMS = ["believer", "education", "essential", "pro", "legacy_pro"]; public static bool IsUserSubscribed(this ClaimsPrincipal user) => user.Claims.Any((c) => c.Type == "notesnook:status" && SUBSCRIBED_CLAIMS.Contains(c.Value)); } diff --git a/Streetwriters.Common/Interfaces/IUserSubscriptionService.cs b/Streetwriters.Common/Interfaces/IUserSubscriptionService.cs index 2ed2abd..a7b2881 100644 --- a/Streetwriters.Common/Interfaces/IUserSubscriptionService.cs +++ b/Streetwriters.Common/Interfaces/IUserSubscriptionService.cs @@ -9,5 +9,6 @@ namespace Streetwriters.Common.Interfaces { [WampProcedure("co.streetwriters.subscriptions.subscriptions.get_user_subscription")] Task GetUserSubscriptionAsync(string clientId, string userId); + Subscription TransformUserSubscription(Subscription subscription); } } \ No newline at end of file diff --git a/Streetwriters.Identity/Controllers/SignupController.cs b/Streetwriters.Identity/Controllers/SignupController.cs index 2fa5643..e0b9a93 100644 --- a/Streetwriters.Identity/Controllers/SignupController.cs +++ b/Streetwriters.Identity/Controllers/SignupController.cs @@ -109,7 +109,7 @@ namespace Streetwriters.Identity.Controllers await UserManager.AddToRoleAsync(user, client.Id); if (Constants.IS_SELF_HOSTED) { - await UserManager.AddClaimAsync(user, UserService.SubscriptionPlanToClaim(client.Id, SubscriptionPlan.BELIEVER)); + await UserManager.AddClaimAsync(user, new Claim(UserService.GetClaimKey(client.Id), "believer")); } else { diff --git a/Streetwriters.Identity/MessageHandlers/CreateSubscription.cs b/Streetwriters.Identity/MessageHandlers/CreateSubscription.cs index 53feb52..618623a 100644 --- a/Streetwriters.Identity/MessageHandlers/CreateSubscription.cs +++ b/Streetwriters.Identity/MessageHandlers/CreateSubscription.cs @@ -30,20 +30,19 @@ namespace Streetwriters.Identity.MessageHandlers { public class CreateSubscription { - public static async Task Process(CreateSubscriptionMessage message, UserManager userManager) + public static async Task Process(Subscription subscription, UserManager userManager) { - var user = await userManager.FindByIdAsync(message.UserId); - var client = Clients.FindClientByAppId(message.AppId); + var user = await userManager.FindByIdAsync(subscription.UserId); + var client = Clients.FindClientByAppId(subscription.AppId); if (client == null || user == null) return; IdentityUserClaim statusClaim = user.Claims.FirstOrDefault((c) => c.ClaimType == UserService.GetClaimKey(client.Id)); - Claim subscriptionClaim = UserService.SubscriptionTypeToClaim(client.Id, message.Type); + Claim subscriptionClaim = UserService.SubscriptionPlanToClaim(client.Id, subscription); if (statusClaim?.ClaimValue == subscriptionClaim.Value) return; if (statusClaim != null) await userManager.ReplaceClaimAsync(user, statusClaim.ToClaim(), subscriptionClaim); - // we no longer accept legacy subscriptions. - // else - // await userManager.AddClaimAsync(user, subscriptionClaim); + else + await userManager.AddClaimAsync(user, subscriptionClaim); } } diff --git a/Streetwriters.Identity/MessageHandlers/CreateSubscriptionV2.cs b/Streetwriters.Identity/MessageHandlers/CreateSubscriptionV2.cs deleted file mode 100644 index ac845eb..0000000 --- a/Streetwriters.Identity/MessageHandlers/CreateSubscriptionV2.cs +++ /dev/null @@ -1,49 +0,0 @@ -/* -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.Tasks; -using Streetwriters.Common.Messages; -using Streetwriters.Common.Models; -using Streetwriters.Common; -using Microsoft.AspNetCore.Identity; -using System.Security.Claims; -using System.Linq; -using Streetwriters.Identity.Services; - -namespace Streetwriters.Identity.MessageHandlers -{ - public class CreateSubscriptionV2 - { - public static async Task Process(CreateSubscriptionMessageV2 message, UserManager userManager) - { - var user = await userManager.FindByIdAsync(message.UserId); - var client = Clients.FindClientByAppId(message.AppId); - if (client == null || user == null) return; - - IdentityUserClaim statusClaim = user.Claims.FirstOrDefault((c) => c.ClaimType == UserService.GetClaimKey(client.Id)); - Claim subscriptionClaim = UserService.SubscriptionPlanToClaim(client.Id, message.Plan); - if (statusClaim?.ClaimValue == subscriptionClaim.Value) return; - if (statusClaim != null) - await userManager.ReplaceClaimAsync(user, statusClaim.ToClaim(), subscriptionClaim); - else - await userManager.AddClaimAsync(user, subscriptionClaim); - } - - } -} \ No newline at end of file diff --git a/Streetwriters.Identity/Services/UserService.cs b/Streetwriters.Identity/Services/UserService.cs index 21546c4..0f0f0cd 100644 --- a/Streetwriters.Identity/Services/UserService.cs +++ b/Streetwriters.Identity/Services/UserService.cs @@ -17,6 +17,7 @@ You should have received a copy of the Affero GNU General Public License along with this program. If not, see . */ +using System; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; @@ -28,27 +29,6 @@ namespace Streetwriters.Identity.Services { public class UserService { - private static SubscriptionType? GetUserSubscriptionStatus(string clientId, User user) - { - var claimKey = GetClaimKey(clientId); - var status = user.Claims.FirstOrDefault((c) => c.ClaimType == claimKey).ClaimValue; - switch (status) - { - case "basic": - return SubscriptionType.BASIC; - case "trial": - return SubscriptionType.TRIAL; - case "premium": - return SubscriptionType.PREMIUM; - case "premium_canceled": - return SubscriptionType.PREMIUM_CANCELED; - case "premium_expired": - return SubscriptionType.PREMIUM_EXPIRED; - default: - return null; - } - } - private static SubscriptionPlan? GetUserSubscriptionPlan(string clientId, User user) { var claimKey = GetClaimKey(clientId); @@ -72,39 +52,23 @@ namespace Streetwriters.Identity.Services public static bool IsSMSMFAAllowed(string clientId, User user) { - var legacyStatus = GetUserSubscriptionStatus(clientId, user); var status = GetUserSubscriptionPlan(clientId, user); - if (legacyStatus == null && status == null) return false; - return legacyStatus == SubscriptionType.PREMIUM || - legacyStatus == SubscriptionType.PREMIUM_CANCELED || + if (status == null) return false; + return status == SubscriptionPlan.LEGACY_PRO || status == SubscriptionPlan.PRO || status == SubscriptionPlan.EDUCATION || status == SubscriptionPlan.BELIEVER; } - public static Claim SubscriptionTypeToClaim(string clientId, SubscriptionType type) + public static Claim SubscriptionPlanToClaim(string clientId, Subscription subscription) { var claimKey = GetClaimKey(clientId); - switch (type) - { - case SubscriptionType.BASIC: - return new Claim(claimKey, "basic"); - case SubscriptionType.TRIAL: - return new Claim(claimKey, "trial"); - case SubscriptionType.PREMIUM: - return new Claim(claimKey, "premium"); - case SubscriptionType.PREMIUM_CANCELED: - return new Claim(claimKey, "premium_canceled"); - case SubscriptionType.PREMIUM_EXPIRED: - return new Claim(claimKey, "premium_expired"); - } - return null; - } - public static Claim SubscriptionPlanToClaim(string clientId, SubscriptionPlan plan) - { - var claimKey = GetClaimKey(clientId); - switch (plan) + // just in case + if (subscription.ExpiryDate <= DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()) + return new Claim(claimKey, "free"); + + switch (subscription.Plan) { case SubscriptionPlan.FREE: return new Claim(claimKey, "free"); @@ -116,6 +80,8 @@ namespace Streetwriters.Identity.Services return new Claim(claimKey, "essential"); case SubscriptionPlan.PRO: return new Claim(claimKey, "pro"); + case SubscriptionPlan.LEGACY_PRO: + return new Claim(claimKey, "legacy_pro"); } return null; } diff --git a/Streetwriters.Identity/Startup.cs b/Streetwriters.Identity/Startup.cs index a45fde3..6d24f0f 100644 --- a/Streetwriters.Identity/Startup.cs +++ b/Streetwriters.Identity/Startup.cs @@ -235,26 +235,16 @@ namespace Streetwriters.Identity { realm.Services.RegisterCallee(() => app.ApplicationServices.CreateScope().ServiceProvider.GetRequiredService()); - realm.Subscribe(SubscriptionServerTopics.CreateSubscriptionTopic, async (CreateSubscriptionMessage message) => + realm.Subscribe(SubscriptionServerTopics.CreateSubscriptionTopic, async (Subscription subscription) => { using (var serviceScope = app.ApplicationServices.CreateScope()) { var services = serviceScope.ServiceProvider; var userManager = services.GetRequiredService>(); - await MessageHandlers.CreateSubscription.Process(message, userManager); + await MessageHandlers.CreateSubscription.Process(subscription, userManager); } }); - realm.Subscribe(SubscriptionServerTopics.CreateSubscriptionV2Topic, async (CreateSubscriptionMessageV2 message) => - { - using (var serviceScope = app.ApplicationServices.CreateScope()) - { - var services = serviceScope.ServiceProvider; - var userManager = services.GetRequiredService>(); - await MessageHandlers.CreateSubscriptionV2.Process(message, userManager); - } - }); - realm.Subscribe(SubscriptionServerTopics.DeleteSubscriptionTopic, async (DeleteSubscriptionMessage message) => { using (var serviceScope = app.ApplicationServices.CreateScope())