[WIP] feat: scoped api keys

This commit is contained in:
Iris System 2025-08-17 02:47:01 -07:00
parent e7ee593a85
commit 06cb160f95
45 changed files with 1264 additions and 154 deletions

View file

@ -16,6 +16,8 @@ public class CoreConfig
public string? SeqLogUrl { get; set; }
public string? DispatchProxyUrl { get; set; }
public string? DispatchProxyToken { get; set; }
public string? InternalApiBaseUrl { get; set; }
public string? InternalApiToken { get; set; }
public LogEventLevel ConsoleLogLevel { get; set; } = LogEventLevel.Debug;
public LogEventLevel ElasticLogLevel { get; set; } = LogEventLevel.Information;

View file

@ -0,0 +1,55 @@
using Dapper;
using SqlKata;
namespace PluralKit.Core;
public partial class ModelRepository
{
public async Task<PKApiKey?> GetApiKey(Guid id)
{
var query = new Query("api_keys")
.Select("id", "system", "scopes", "app", "name", "created")
.SelectRaw("[kind]::text")
.Where("id", id);
return await _db.QueryFirst<PKApiKey?>(query);
}
public async Task<PKApiKey?> GetApiKeyByName(SystemId system, string name)
{
var query = new Query("api_keys")
.Select("id", "system", "scopes", "app", "name", "created")
.SelectRaw("[kind]::text")
.Where("system", system)
.WhereRaw("lower(name) = lower(?)", name.ToLower());
return await _db.QueryFirst<PKApiKey?>(query);
}
public IAsyncEnumerable<PKApiKey> GetSystemApiKeys(SystemId system)
{
var query = new Query("api_keys")
.Select("id", "system", "scopes", "app", "name", "created")
.SelectRaw("[kind]::text")
.Where("system", system)
.WhereRaw("[kind]::text not in ( 'dashboard' )")
.OrderByDesc("created");
return _db.QueryStream<PKApiKey>(query);
}
public async Task UpdateApiKey(Guid id, ApiKeyPatch patch)
{
_logger.Information("Updated API key {keyId}: {@ApiKeyPatch}", id, patch);
var query = patch.Apply(new Query("api_keys").Where("id", id));
await _db.ExecuteQuery(query, "returning *");
}
public async Task DeleteApiKey(Guid id)
{
var query = new Query("api_keys").AsDelete().Where("id", id);
await _db.ExecuteQuery(query);
_logger.Information("Deleted ApiKey {keyId}", id);
}
}

View file

@ -0,0 +1,15 @@
using NodaTime;
namespace PluralKit.Core;
public class PKApiKey
{
public Guid Id { get; private set; }
public SystemId System { get; private set; }
public string Kind { get; private set; }
public string[] Scopes { get; private set; }
public Guid? App { get; private set; }
public string Name { get; private set; }
public Instant Created { get; private set; }
}

View file

@ -0,0 +1,24 @@
using Newtonsoft.Json.Linq;
using SqlKata;
namespace PluralKit.Core;
public class ApiKeyPatch: PatchObject
{
public Partial<string> Name { get; set; }
public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper
.With("name", Name)
);
public JObject ToJson()
{
var o = new JObject();
if (Name.IsPresent)
o.Add("name", Name.Value);
return o;
}
}

View file

@ -0,0 +1,72 @@
using Autofac;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NodaTime;
using Serilog;
namespace PluralKit.Core;
public class ApiKeyService
{
private readonly HttpClient _client;
private readonly ILogger _logger;
private readonly CoreConfig _cfg;
private readonly ILifetimeScope _provider;
public ApiKeyService(ILogger logger, ILifetimeScope provider, CoreConfig cfg)
{
_logger = logger;
_cfg = cfg;
_provider = provider;
_client = new HttpClient();
_client.DefaultRequestHeaders.Add("User-Agent", "PluralKitInternal");
}
public async Task<string?> CreateUserApiKey(SystemId systemId, string keyName, string[] keyScopes, bool check = false)
{
if (_cfg.InternalApiBaseUrl == null || _cfg.InternalApiToken == null)
throw new Exception("internal API config not set!");
if (!Uri.TryCreate(new Uri(_cfg.InternalApiBaseUrl), "/internal/apikey/user", out var uri))
throw new Exception("internal API base invalid!?");
var repo = _provider.Resolve<ModelRepository>();
var system = await repo.GetSystem(systemId);
if (system == null)
return null;
var reqData = new JObject();
reqData.Add("check", check);
reqData.Add("system", system.Id.Value);
reqData.Add("name", keyName);
reqData.Add("scopes", new JArray(keyScopes));
var req = new HttpRequestMessage()
{
RequestUri = uri,
Method = HttpMethod.Post,
Content = new StringContent(JsonConvert.SerializeObject(reqData), Encoding.UTF8, "application/json"),
};
req.Headers.Add("X-Pluralkit-InternalAuth", _cfg.InternalApiToken);
var res = await _client.SendAsync(req);
var data = JsonConvert.DeserializeObject<JObject>(await res.Content.ReadAsStringAsync());
if (data.ContainsKey("error"))
throw new Exception($"API key validation failed: {(data.Value<string>("error"))}");
if (data.Value<bool>("valid") != true)
throw new Exception("API key validation failed: unknown error");
if (!data.ContainsKey("token"))
return null;
return data.Value<string>("token");
}
}