diff --git a/extra/admin-api/Spacebar.AdminAPI/Controllers/GuildController.cs b/extra/admin-api/Spacebar.AdminAPI/Controllers/GuildController.cs index a15dd2598..03b38e64d 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Controllers/GuildController.cs +++ b/extra/admin-api/Spacebar.AdminAPI/Controllers/GuildController.cs @@ -1,10 +1,8 @@ -using System.Diagnostics; -using ArcaneLibs; using ArcaneLibs.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using RabbitMQ.Client; +using Spacebar.AdminAPI.Extensions; using Spacebar.AdminApi.Models; using Spacebar.AdminAPI.Services; using Spacebar.Db.Contexts; @@ -15,12 +13,14 @@ namespace Spacebar.AdminAPI.Controllers; [ApiController] [Route("/Guilds")] -public class GuildController(ILogger logger, Configuration config, RabbitMQConfiguration amqpConfig, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp, AuthenticationService authService) : ControllerBase { +public class GuildController(ILogger logger, Configuration config, RabbitMQConfiguration amqpConfig, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp, AuthenticationService auth) : ControllerBase { private readonly ILogger _logger = logger; [HttpGet] - public IAsyncEnumerable Get() { - return db.Guilds.Select(x => new GuildModel { + public async IAsyncEnumerable Get() { + (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); + + var results = db.Guilds.Select(x => new GuildModel { Id = x.Id, AfkChannelId = x.AfkChannelId, AfkTimeout = x.AfkTimeout, @@ -70,16 +70,21 @@ public class GuildController(ILogger logger, Configuration conf BanCount = x.Bans.Count(), VoiceStateCount = x.VoiceStates.Count(), }).AsAsyncEnumerable(); + await foreach (var result in results) { + yield return result; + } } [HttpPost("{id}/force_join")] public async Task ForceJoinGuild([FromBody] ForceJoinRequest request, string id) { + (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); + var guild = await db.Guilds.FindAsync(id); if (guild == null) { return NotFound(new { entity = "Guild", id, message = "Guild not found" }); } - var userId = request.UserId ?? config.OverrideUid ?? (await authService.GetCurrentUser(Request)).Id; + var userId = request.UserId ?? config.OverrideUid ?? (await auth.GetCurrentUser(Request)).Id; var user = await db.Users.FindAsync(userId); if (user == null) { return NotFound(new { entity = "User", id = userId, message = "User not found" }); @@ -138,6 +143,8 @@ public class GuildController(ILogger logger, Configuration conf [HttpGet("{id}/delete")] public async IAsyncEnumerable DeleteUser(string id, [FromQuery] int messageDeleteChunkSize = 100) { + (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); + var user = await db.Users.FindAsync(id); if (user == null) { Console.WriteLine($"User {id} not found"); diff --git a/extra/admin-api/Spacebar.AdminAPI/Controllers/Media/UserMediaController.cs b/extra/admin-api/Spacebar.AdminAPI/Controllers/Media/UserMediaController.cs index a06d110bb..a4d915e5a 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Controllers/Media/UserMediaController.cs +++ b/extra/admin-api/Spacebar.AdminAPI/Controllers/Media/UserMediaController.cs @@ -1,6 +1,8 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Spacebar.AdminAPI.Extensions; using Spacebar.AdminApi.Models; +using Spacebar.AdminAPI.Services; using Spacebar.Db.Contexts; using Spacebar.Db.Models; using Spacebar.RabbitMqUtilities; @@ -9,10 +11,11 @@ namespace Spacebar.AdminAPI.Controllers.Media; [ApiController] [Route("/media/user")] -public class UserMediaController(ILogger logger, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp) : ControllerBase { - +public class UserMediaController(ILogger logger, SpacebarDbContext db, RabbitMQService mq, AuthenticationService auth, IServiceProvider sp) : ControllerBase { [HttpGet("{userId}/attachments")] public async IAsyncEnumerable GetAttachmentsByUser(string userId) { + (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); + var db2 = sp.CreateScope().ServiceProvider.GetService(); var attachments = db.Attachments // .IgnoreAutoIncludes() diff --git a/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs b/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs index e4018a1ee..436c0f91f 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs +++ b/extra/admin-api/Spacebar.AdminAPI/Controllers/UserController.cs @@ -4,6 +4,7 @@ using ArcaneLibs.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RabbitMQ.Client; +using Spacebar.AdminAPI.Extensions; using Spacebar.AdminApi.Models; using Spacebar.AdminAPI.Services; using Spacebar.Db.Contexts; @@ -14,12 +15,14 @@ namespace Spacebar.AdminAPI.Controllers; [ApiController] [Route("/users")] -public class UserController(ILogger logger, Configuration config, RabbitMQConfiguration amqpConfig, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp) : ControllerBase { +public class UserController(ILogger logger, Configuration config, RabbitMQConfiguration amqpConfig, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp, AuthenticationService auth) : ControllerBase { private readonly ILogger _logger = logger; [HttpGet] - public IAsyncEnumerable Get() { - return db.Users.Select(x => new UserModel { + public async IAsyncEnumerable Get() { + (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); + + var results = db.Users.Select(x => new UserModel { Id = x.Id, Username = x.Username, Discriminator = x.Discriminator, @@ -57,10 +60,15 @@ public class UserController(ILogger logger, Configuration config GuildCount = x.Guilds.Count, OwnedGuildCount = x.Guilds.Count(g => g.OwnerId == x.Id) }).AsAsyncEnumerable(); + + await foreach (var user in results) { + yield return user; + } } [HttpGet("meow")] public async Task Meow() { + (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); Console.WriteLine("meow"); ConnectionFactory factory = new ConnectionFactory(); @@ -141,6 +149,8 @@ public class UserController(ILogger logger, Configuration config [HttpGet("{id}/delete")] public async IAsyncEnumerable DeleteUser(string id, [FromQuery] int messageDeleteChunkSize = 100) { + (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); + var user = await db.Users.FindAsync(id); if (user == null) { Console.WriteLine($"User {id} not found"); @@ -248,6 +258,8 @@ public class UserController(ILogger logger, Configuration config [HttpGet("duplicate")] public async Task Duplicate() { + (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); + var msg = db.Messages.First(); var channels = db.Channels.Select(x => new { x.Id, x.GuildId }).ToList(); int count = 1; @@ -291,6 +303,8 @@ public class UserController(ILogger logger, Configuration config [HttpGet("duplicate/{id}")] public async Task DuplicateMessage(ulong id, [FromQuery] int count = 100) { + (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); + var msg = await db.Messages.FindAsync(id.ToString()); int createdCount = 1; while (true) { @@ -335,6 +349,8 @@ public class UserController(ILogger logger, Configuration config [HttpGet("truncate_messages")] public async Task TruncateMessages() { + (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); + var channels = db.Channels.Select(x => new { x.Id, x.GuildId }).ToList(); var ss = new SemaphoreSlim(12, 12); @@ -364,6 +380,8 @@ public class UserController(ILogger logger, Configuration config } private async IAsyncEnumerable AggregateAsyncEnumerablesWithoutOrder(params IEnumerable> enumerables) { + (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); + var enumerators = enumerables.Select(e => e.GetAsyncEnumerator()).ToList(); var tasks = enumerators.Select(e => e.MoveNextAsync().AsTask()).ToList(); @@ -439,6 +457,8 @@ public class UserController(ILogger logger, Configuration config [HttpGet("test")] public async IAsyncEnumerable Test() { + (await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); + var factory = new ConnectionFactory { Uri = new Uri(amqpConfig.ToConnectionString()) }; diff --git a/extra/admin-api/Spacebar.AdminAPI/Extensions/DbExtensions.cs b/extra/admin-api/Spacebar.AdminAPI/Extensions/DbExtensions.cs index c97a15759..f6318e8f3 100644 --- a/extra/admin-api/Spacebar.AdminAPI/Extensions/DbExtensions.cs +++ b/extra/admin-api/Spacebar.AdminAPI/Extensions/DbExtensions.cs @@ -1,8 +1,10 @@ using Microsoft.EntityFrameworkCore; +using Spacebar.AdminApi.Models; using Spacebar.Db.Models; namespace Spacebar.AdminAPI.Extensions; public static class DbExtensions { public static string? GetString(this DbSet config, string key) => config.Find(key)?.Value; + public static SpacebarRights.Rights GetRights(this User user) => (SpacebarRights.Rights)user.Rights; } \ No newline at end of file diff --git a/extra/admin-api/Spacebar.AdminApi.Models/Rights.cs b/extra/admin-api/Spacebar.AdminApi.Models/Rights.cs index e69de29bb..ef82cb500 100644 --- a/extra/admin-api/Spacebar.AdminApi.Models/Rights.cs +++ b/extra/admin-api/Spacebar.AdminApi.Models/Rights.cs @@ -0,0 +1,81 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Spacebar.AdminApi.Models; + +public static class SpacebarRights { + [Flags] + [SuppressMessage("ReSharper", "InconsistentNaming")] + public enum Rights : ulong { + OPERATOR = 1ul << 0, // has all rights + MANAGE_APPLICATIONS = 1ul << 1, + MANAGE_GUILDS = 1ul << 2, // Manage all guilds instance-wide + MANAGE_MESSAGES = 1ul << 3, // Can't see other messages but delete/edit them in channels that they can see + MANAGE_RATE_LIMITS = 1ul << 4, + MANAGE_ROUTING = 1ul << 5, // can create custom message routes to any channel/guild + MANAGE_TICKETS = 1ul << 6, // can respond to and resolve support tickets + MANAGE_USERS = 1ul << 7, + ADD_MEMBERS = 1ul << 8, // can manually add any members in their guilds + BYPASS_RATE_LIMITS = 1ul << 9, + CREATE_APPLICATIONS = 1ul << 10, + CREATE_CHANNELS = 1ul << 11, // can create guild channels or threads in the guilds that they have permission + CREATE_DMS = 1ul << 12, + CREATE_DM_GROUPS = 1ul << 13, // can create group DMs or custom orphan channels + CREATE_GUILDS = 1ul << 14, + CREATE_INVITES = 1ul << 15, // can create mass invites in the guilds that they have CREATE_INSTANT_INVITE + CREATE_ROLES = 1ul << 16, + CREATE_TEMPLATES = 1ul << 17, + CREATE_WEBHOOKS = 1ul << 18, + JOIN_GUILDS = 1ul << 19, + PIN_MESSAGES = 1ul << 20, + SELF_ADD_REACTIONS = 1ul << 21, + SELF_DELETE_MESSAGES = 1ul << 22, + SELF_EDIT_MESSAGES = 1ul << 23, + SELF_EDIT_NAME = 1ul << 24, + SEND_MESSAGES = 1ul << 25, + USE_ACTIVITIES = 1ul << 26, // use (game) activities in voice channels (e.g. Watch together) + USE_VIDEO = 1ul << 27, + USE_VOICE = 1ul << 28, + INVITE_USERS = 1ul << 29, // can create user-specific invites in the guilds that they have INVITE_USERS + SELF_DELETE_DISABLE = 1ul << 30, // can disable/delete own account + DEBTABLE = 1ul << 31, // can use pay-to-use features + CREDITABLE = 1ul << 32, // can receive money from monetisation related features + KICK_BAN_MEMBERS = 1ul << 33, + + // can kick or ban guild or group DM members in the guilds/groups that they have KICK_MEMBERS, or BAN_MEMBERS + SELF_LEAVE_GROUPS = 1ul << 34, + + // can leave the guilds or group DMs that they joined on their own (one can always leave a guild or group DMs they have been force-added) + PRESENCE = 1ul << 35, + + // inverts the presence confidentiality default (OPERATOR's presence is not routed by default, others' are) for a given user + SELF_ADD_DISCOVERABLE = 1ul << 36, // can mark discoverable guilds that they have permissions to mark as discoverable + MANAGE_GUILD_DIRECTORY = 1ul << 37, // can change anything in the primary guild directory + POGGERS = 1ul << 38, // can send confetti, screenshake, random user mention (@someone) + USE_ACHIEVEMENTS = 1ul << 39, // can use achievements and cheers + INITIATE_INTERACTIONS = 1ul << 40, // can initiate interactions + RESPOND_TO_INTERACTIONS = 1ul << 41, // can respond to interactions + SEND_BACKDATED_EVENTS = 1ul << 42, // can send backdated events + USE_MASS_INVITES = 1ul << 43, // added per @xnacly's request - can accept mass invites + ACCEPT_INVITES = 1ul << 44, // added per @xnacly's request - can accept user-specific invites and DM requests + SELF_EDIT_FLAGS = 1ul << 45, // can modify own flags + EDIT_FLAGS = 1ul << 46, // can set others' flags + MANAGE_GROUPS = 1ul << 47, // can manage others' groups + VIEW_SERVER_STATS = 1ul << 48, // added per @chrischrome's request - can view server stats + RESEND_VERIFICATION_EMAIL = 1ul << 49, // can resend verification emails (/auth/verify/resend) + CREATE_REGISTRATION_TOKENS = 1ul << 50, // can create registration tokens (/auth/generate-registration-tokens) + } + + public static bool HasAllRights(this Rights val, Rights rights) { + if (val.HasFlag(Rights.OPERATOR)) { + return true; + } + + return (val & rights) == rights; + } + + public static void AssertHasAllRights(this Rights val, Rights rights) { + if (!val.HasAllRights(rights)) { + throw new UnauthorizedAccessException("Insufficient rights: missing " + rights); + } + } +} \ No newline at end of file diff --git a/src/util/util/Rights.ts b/src/util/util/Rights.ts index 365eb7205..caaa5b55e 100644 --- a/src/util/util/Rights.ts +++ b/src/util/util/Rights.ts @@ -29,7 +29,7 @@ export type RightResolvable = | RightString; type RightString = keyof typeof Rights.FLAGS; -// TODO: just like roles for members, users should have privilidges which combine multiple rights into one and make it easy to assign +// TODO: just like roles for members, users should have priviliges which combine multiple rights into one and make it easy to assign export class Rights extends BitField { constructor(bits: BitFieldResolvable = 0) { @@ -86,12 +86,12 @@ export class Rights extends BitField { INITIATE_INTERACTIONS: BitFlag(40), // can initiate interactions RESPOND_TO_INTERACTIONS: BitFlag(41), // can respond to interactions SEND_BACKDATED_EVENTS: BitFlag(42), // can send backdated events - USE_MASS_INVITES: BitFlag(43), // added per @xnacly's request — can accept mass invites - ACCEPT_INVITES: BitFlag(44), // added per @xnacly's request — can accept user-specific invites and DM requests + USE_MASS_INVITES: BitFlag(43), // added per @xnacly's request - can accept mass invites + ACCEPT_INVITES: BitFlag(44), // added per @xnacly's request - can accept user-specific invites and DM requests SELF_EDIT_FLAGS: BitFlag(45), // can modify own flags EDIT_FLAGS: BitFlag(46), // can set others' flags MANAGE_GROUPS: BitFlag(47), // can manage others' groups - VIEW_SERVER_STATS: BitFlag(48), // added per @chrischrome's request — can view server stats) + VIEW_SERVER_STATS: BitFlag(48), // added per @chrischrome's request - can view server stats) RESEND_VERIFICATION_EMAIL: BitFlag(49), // can resend verification emails (/auth/verify/resend) CREATE_REGISTRATION_TOKENS: BitFlag(50), // can create registration tokens (/auth/generate-registration-tokens) };