Admin API: Check rights, require OPERATOR

This commit is contained in:
Rory&
2025-11-24 20:24:17 +01:00
parent 814f532160
commit eb07c5c956
6 changed files with 129 additions and 16 deletions

View File

@@ -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<GuildController> logger, Configuration config, RabbitMQConfiguration amqpConfig, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp, AuthenticationService authService) : ControllerBase {
public class GuildController(ILogger<GuildController> logger, Configuration config, RabbitMQConfiguration amqpConfig, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp, AuthenticationService auth) : ControllerBase {
private readonly ILogger<GuildController> _logger = logger;
[HttpGet]
public IAsyncEnumerable<GuildModel> Get() {
return db.Guilds.Select(x => new GuildModel {
public async IAsyncEnumerable<GuildModel> 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<GuildController> 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<IActionResult> 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<GuildController> logger, Configuration conf
[HttpGet("{id}/delete")]
public async IAsyncEnumerable<AsyncActionResult> 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");

View File

@@ -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<UserMediaController> logger, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp) : ControllerBase {
public class UserMediaController(ILogger<UserMediaController> logger, SpacebarDbContext db, RabbitMQService mq, AuthenticationService auth, IServiceProvider sp) : ControllerBase {
[HttpGet("{userId}/attachments")]
public async IAsyncEnumerable<Attachment> GetAttachmentsByUser(string userId) {
(await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR);
var db2 = sp.CreateScope().ServiceProvider.GetService<SpacebarDbContext>();
var attachments = db.Attachments
// .IgnoreAutoIncludes()

View File

@@ -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<UserController> logger, Configuration config, RabbitMQConfiguration amqpConfig, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp) : ControllerBase {
public class UserController(ILogger<UserController> logger, Configuration config, RabbitMQConfiguration amqpConfig, SpacebarDbContext db, RabbitMQService mq, IServiceProvider sp, AuthenticationService auth) : ControllerBase {
private readonly ILogger<UserController> _logger = logger;
[HttpGet]
public IAsyncEnumerable<UserModel> Get() {
return db.Users.Select(x => new UserModel {
public async IAsyncEnumerable<UserModel> 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<UserController> 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<UserController> logger, Configuration config
[HttpGet("{id}/delete")]
public async IAsyncEnumerable<AsyncActionResult> 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<UserController> logger, Configuration config
[HttpGet("duplicate")]
public async Task<IActionResult> 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<UserController> logger, Configuration config
[HttpGet("duplicate/{id}")]
public async Task<IActionResult> 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<UserController> 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<UserController> logger, Configuration config
}
private async IAsyncEnumerable<T> AggregateAsyncEnumerablesWithoutOrder<T>(params IEnumerable<IAsyncEnumerable<T>> 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<UserController> logger, Configuration config
[HttpGet("test")]
public async IAsyncEnumerable<string> Test() {
(await auth.GetCurrentUser(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR);
var factory = new ConnectionFactory {
Uri = new Uri(amqpConfig.ToConnectionString())
};

View File

@@ -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> config, string key) => config.Find(key)?.Value;
public static SpacebarRights.Rights GetRights(this User user) => (SpacebarRights.Rights)user.Rights;
}

View File

@@ -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);
}
}
}

View File

@@ -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)
};