From 3a466b2e0c6d137e7b98003750a83f192b704879 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Sat, 27 Jan 2024 23:50:31 +0100 Subject: [PATCH] Add support for registration invites --- .../Controllers/AuthController.cs | 14 ++++++---- .../Controllers/Mastodon/AuthController.cs | 21 -------------- .../Mastodon/MastodonAuthController.cs | 11 ++++++++ .../Controllers/Schemas/AuthRequest.cs | 5 ++-- .../Core/Configuration/Config.cs | 5 ++-- .../Core/Configuration/Constants.cs | 6 ++-- Iceshrimp.Backend/Core/Configuration/Enums.cs | 9 ++++++ .../ActivityStreams/Types/ASActivity.cs | 3 +- .../ActivityStreams/Types/ASActor.cs | 3 +- .../ActivityStreams/Types/ASNote.cs | 3 +- .../Core/Services/UserService.cs | 28 ++++++++++++++----- Iceshrimp.Backend/configuration.ini | 6 +++- 12 files changed, 70 insertions(+), 44 deletions(-) delete mode 100644 Iceshrimp.Backend/Controllers/Mastodon/AuthController.cs create mode 100644 Iceshrimp.Backend/Controllers/Mastodon/MastodonAuthController.cs create mode 100644 Iceshrimp.Backend/Core/Configuration/Enums.cs diff --git a/Iceshrimp.Backend/Controllers/AuthController.cs b/Iceshrimp.Backend/Controllers/AuthController.cs index 422115eb..1be83cdc 100644 --- a/Iceshrimp.Backend/Controllers/AuthController.cs +++ b/Iceshrimp.Backend/Controllers/AuthController.cs @@ -28,14 +28,17 @@ public class AuthController(DatabaseContext db, UserService userSvc) : Controlle [Consumes(MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TimelineResponse))] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))] - [ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(ErrorResponse))] + [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(ErrorResponse))] public async Task Login([FromBody] AuthRequest request) { var user = await db.Users.FirstOrDefaultAsync(p => p.Host == null && p.UsernameLower == request.Username.ToLowerInvariant()); - if (user == null) return Unauthorized(); + if (user == null) + return StatusCode(StatusCodes.Status403Forbidden); var profile = await db.UserProfiles.FirstOrDefaultAsync(p => p.User == user); - if (profile?.Password == null) return Unauthorized(); - if (!AuthHelpers.ComparePassword(request.Password, profile.Password)) return Unauthorized(); + if (profile?.Password == null) + return StatusCode(StatusCodes.Status403Forbidden); + if (!AuthHelpers.ComparePassword(request.Password, profile.Password)) + return StatusCode(StatusCodes.Status403Forbidden); var res = await db.AddAsync(new Session { Id = IdHelpers.GenerateSlowflakeId(), @@ -65,11 +68,12 @@ public class AuthController(DatabaseContext db, UserService userSvc) : Controlle [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TimelineResponse))] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))] [ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(ErrorResponse))] + [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(ErrorResponse))] public async Task Register([FromBody] AuthRequest request) { //TODO: captcha support //TODO: invite support - await userSvc.CreateLocalUser(request.Username, request.Password); + await userSvc.CreateLocalUser(request.Username, request.Password, request.Invite); return await Login(request); } diff --git a/Iceshrimp.Backend/Controllers/Mastodon/AuthController.cs b/Iceshrimp.Backend/Controllers/Mastodon/AuthController.cs deleted file mode 100644 index 6f490f98..00000000 --- a/Iceshrimp.Backend/Controllers/Mastodon/AuthController.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Net; -using System.Net.Mime; -using Iceshrimp.Backend.Controllers.Schemas; -using Iceshrimp.Backend.Core.Database; -using Iceshrimp.Backend.Core.Database.Tables; -using Iceshrimp.Backend.Core.Helpers; -using Iceshrimp.Backend.Core.Services; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.RateLimiting; -using Microsoft.EntityFrameworkCore; - -namespace Iceshrimp.Backend.Controllers.Mastodon; - -[ApiController] -[Tags("Mastodon")] -[EnableRateLimiting("sliding")] -[Produces("application/json")] -[Route("/api/v1")] -public class MastodonAuthController() : Controller { - -} \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Mastodon/MastodonAuthController.cs b/Iceshrimp.Backend/Controllers/Mastodon/MastodonAuthController.cs new file mode 100644 index 00000000..3d08ae99 --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Mastodon/MastodonAuthController.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; + +namespace Iceshrimp.Backend.Controllers.Mastodon; + +[ApiController] +[Tags("Mastodon")] +[EnableRateLimiting("sliding")] +[Produces("application/json")] +[Route("/api/v1")] +public class MastodonAuthController : Controller { } \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Schemas/AuthRequest.cs b/Iceshrimp.Backend/Controllers/Schemas/AuthRequest.cs index 67e882f2..3b8df4cc 100644 --- a/Iceshrimp.Backend/Controllers/Schemas/AuthRequest.cs +++ b/Iceshrimp.Backend/Controllers/Schemas/AuthRequest.cs @@ -3,6 +3,7 @@ using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; namespace Iceshrimp.Backend.Controllers.Schemas; public class AuthRequest { - [J("username")] public required string Username { get; set; } - [J("password")] public required string Password { get; set; } + [J("username")] public required string Username { get; set; } + [J("password")] public required string Password { get; set; } + [J("invite")] public string? Invite { get; set; } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Configuration/Config.cs b/Iceshrimp.Backend/Core/Configuration/Config.cs index 20809e57..58d64390 100644 --- a/Iceshrimp.Backend/Core/Configuration/Config.cs +++ b/Iceshrimp.Backend/Core/Configuration/Config.cs @@ -36,8 +36,9 @@ public sealed class Config { } public sealed class SecuritySection { - public required bool AuthorizedFetch { get; init; } = true; - public required ExceptionVerbosity ExceptionVerbosity { get; init; } = ExceptionVerbosity.Basic; + public required bool AuthorizedFetch { get; init; } = true; + public required ExceptionVerbosity ExceptionVerbosity { get; init; } = ExceptionVerbosity.Basic; + public required Enums.Registrations Registrations { get; init; } = Enums.Registrations.Closed; } public sealed class DatabaseSection { diff --git a/Iceshrimp.Backend/Core/Configuration/Constants.cs b/Iceshrimp.Backend/Core/Configuration/Constants.cs index c1899441..c6c1f180 100644 --- a/Iceshrimp.Backend/Core/Configuration/Constants.cs +++ b/Iceshrimp.Backend/Core/Configuration/Constants.cs @@ -1,6 +1,6 @@ namespace Iceshrimp.Backend.Core.Configuration; -public class Constants { - public const string UserAgent = "Iceshrimp/2024.1-experimental (https://shrimp-next.fedi.solutions)"; - public static readonly string[] SystemUsers = { "instance.actor", "relay.actor" }; +public static class Constants { + public const string ActivityStreamsNs = "https://www.w3.org/ns/activitystreams"; + public static readonly string[] SystemUsers = { "instance.actor", "relay.actor" }; } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Configuration/Enums.cs b/Iceshrimp.Backend/Core/Configuration/Enums.cs new file mode 100644 index 00000000..07fe36a6 --- /dev/null +++ b/Iceshrimp.Backend/Core/Configuration/Enums.cs @@ -0,0 +1,9 @@ +namespace Iceshrimp.Backend.Core.Configuration; + +public static class Enums { + public enum Registrations { + Closed = 0, + Invite = 1, + Open = 2 + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActivity.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActivity.cs index 92a1ae6c..54393077 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActivity.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActivity.cs @@ -1,3 +1,4 @@ +using Iceshrimp.Backend.Core.Configuration; using J = Newtonsoft.Json.JsonPropertyAttribute; using JC = Newtonsoft.Json.JsonConverterAttribute; @@ -13,7 +14,7 @@ public class ASActivity : ASObject { public ASObject? Object { get; set; } public static class Types { - private const string Ns = "https://www.w3.org/ns/activitystreams"; + private const string Ns = Constants.ActivityStreamsNs; public const string Create = $"{Ns}#Create"; public const string Delete = $"{Ns}#Delete"; diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActor.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActor.cs index 8520c37f..166a11fd 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActor.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActor.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Extensions; using J = Newtonsoft.Json.JsonPropertyAttribute; using JC = Newtonsoft.Json.JsonConverterAttribute; @@ -134,7 +135,7 @@ public class ASActor : ASObject { } public static class Types { - private const string Ns = "https://www.w3.org/ns/activitystreams"; + private const string Ns = Constants.ActivityStreamsNs; public const string Application = $"{Ns}#Application"; public const string Group = $"{Ns}#Group"; diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs index 673eed82..ffb8f900 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASNote.cs @@ -1,3 +1,4 @@ +using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Database.Tables; using J = Newtonsoft.Json.JsonPropertyAttribute; using JC = Newtonsoft.Json.JsonConverterAttribute; @@ -55,7 +56,7 @@ public class ASNote : ASObject { } public static class Types { - private const string Ns = "https://www.w3.org/ns/activitystreams"; + private const string Ns = Constants.ActivityStreamsNs; public const string Note = $"{Ns}#Note"; } diff --git a/Iceshrimp.Backend/Core/Services/UserService.cs b/Iceshrimp.Backend/Core/Services/UserService.cs index 61ddb669..f6ee51c5 100644 --- a/Iceshrimp.Backend/Core/Services/UserService.cs +++ b/Iceshrimp.Backend/Core/Services/UserService.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Security.Cryptography; using Iceshrimp.Backend.Core.Configuration; @@ -13,7 +14,9 @@ using Microsoft.Extensions.Options; namespace Iceshrimp.Backend.Core.Services; public class UserService( - IOptions config, + [SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor")] + IOptionsSnapshot security, + IOptions instance, ILogger logger, DatabaseContext db, ActivityFetcherService fetchSvc) { @@ -28,8 +31,8 @@ public class UserService( public async Task GetUserFromQuery(string query) { if (query.StartsWith("http://") || query.StartsWith("https://")) - if (query.StartsWith($"https://{config.Value.WebDomain}/users/")) { - query = query[$"https://{config.Value.WebDomain}/users/".Length..]; + if (query.StartsWith($"https://{instance.Value.WebDomain}/users/")) { + query = query[$"https://{instance.Value.WebDomain}/users/".Length..]; return await db.Users.FirstOrDefaultAsync(p => p.Id == query) ?? throw GracefulException.NotFound("User not found"); } @@ -38,7 +41,7 @@ public class UserService( } var tuple = AcctToTuple(query); - if (tuple.Host == config.Value.WebDomain || tuple.Host == config.Value.AccountDomain) + if (tuple.Host == instance.Value.WebDomain || tuple.Host == instance.Value.AccountDomain) tuple.Host = null; return await db.Users.FirstOrDefaultAsync(p => p.Username == tuple.Username && p.Host == tuple.Host); } @@ -97,13 +100,21 @@ public class UserService( return user; } - public async Task CreateLocalUser(string username, string password) { + public async Task CreateLocalUser(string username, string password, string? invite) { + //TODO: invite system should allow multi-use invites & time limited invites + if (security.Value.Registrations == Enums.Registrations.Closed) + throw new GracefulException(HttpStatusCode.Forbidden, "Registrations are disabled on this server"); + if (security.Value.Registrations == Enums.Registrations.Invite && invite == null) + throw new GracefulException(HttpStatusCode.Forbidden, "Request is missing the invite code"); + if (security.Value.Registrations == Enums.Registrations.Invite && + !await db.RegistrationTickets.AnyAsync(p => p.Code == invite)) + throw new GracefulException(HttpStatusCode.Forbidden, "The specified invite code is invalid"); if (username.Contains('.')) throw new GracefulException(HttpStatusCode.BadRequest, "Username must not contain the dot character"); - + if (Constants.SystemUsers.Contains(username.ToLowerInvariant())) + throw new GracefulException(HttpStatusCode.BadRequest, "Username must not be a system user"); if (await db.Users.AnyAsync(p => p.Host == null && p.UsernameLower == username.ToLowerInvariant())) throw new GracefulException(HttpStatusCode.BadRequest, "User already exists"); - if (await db.UsedUsernames.AnyAsync(p => p.Username.ToLower() == username.ToLowerInvariant())) throw new GracefulException(HttpStatusCode.BadRequest, "Username was already used"); @@ -132,6 +143,9 @@ public class UserService( Username = username.ToLowerInvariant() }; + var ticket = await db.RegistrationTickets.FirstAsync(p => p.Code == invite); + + db.Remove(ticket); await db.AddRangeAsync(user, userKeypair, userProfile, usedUsername); await db.SaveChangesAsync(); diff --git a/Iceshrimp.Backend/configuration.ini b/Iceshrimp.Backend/configuration.ini index 6872c4df..48a7d12a 100644 --- a/Iceshrimp.Backend/configuration.ini +++ b/Iceshrimp.Backend/configuration.ini @@ -7,10 +7,14 @@ AccountDomain = example.org ;; Whether to require incoming ActivityPub requests carry a valid HTTP or LD signature AuthorizedFetch = true -;; The level of detail in API error responses. +;; The level of detail in API error responses ;; Options: [None, Basic, Full] ExceptionVerbosity = Basic +;; Whether to allow instance registrations +;; Options: [Closed, Invite, Open] +Registrations = Closed + [Database] Host = localhost Database = iceshrimp