Migrate API to ASP.NET Core Auth services + refactor

This commit is contained in:
Ske
2020-06-16 01:15:59 +02:00
parent 7fde54050a
commit 627f544ee8
25 changed files with 289 additions and 141 deletions

View File

@@ -15,12 +15,10 @@ namespace PluralKit.API
public class AccountController: ControllerBase
{
private IDataStore _data;
private TokenAuthService _auth;
public AccountController(IDataStore data, TokenAuthService auth)
public AccountController(IDataStore data)
{
_data = data;
_auth = auth;
}
[HttpGet("{aid}")]
@@ -29,7 +27,7 @@ namespace PluralKit.API
var system = await _data.GetSystemByAccount(aid);
if (system == null) return NotFound("Account not found.");
return Ok(system.ToJson(_auth.ContextFor(system)));
return Ok(system.ToJson(User.ContextFor(system)));
}
}
}

View File

@@ -0,0 +1,131 @@
using System;
using System.Linq;
using Newtonsoft.Json.Linq;
using PluralKit.Core;
namespace PluralKit.API
{
public static class JsonModelExt
{
public static JObject ToJson(this PKSystem system, LookupContext ctx)
{
var o = new JObject();
o.Add("id", system.Hid);
o.Add("name", system.Name);
o.Add("description", system.DescriptionPrivacy.CanAccess(ctx) ? system.Description : null);
o.Add("tag", system.Tag);
o.Add("avatar_url", system.AvatarUrl);
o.Add("created", DateTimeFormats.TimestampExportFormat.Format(system.Created));
o.Add("tz", system.UiTz);
o.Add("description_privacy", ctx == LookupContext.ByOwner ? system.DescriptionPrivacy.ToJsonString() : null);
o.Add("member_list_privacy", ctx == LookupContext.ByOwner ? system.MemberListPrivacy.ToJsonString() : null);
o.Add("front_privacy", ctx == LookupContext.ByOwner ? system.FrontPrivacy.ToJsonString() : null);
o.Add("front_history_privacy", ctx == LookupContext.ByOwner ? ctx == LookupContext.ByOwner ? system.FrontHistoryPrivacy.ToJsonString() : null : null);
return o;
}
public static void ApplyJson(this PKSystem system, JObject o)
{
if (o.ContainsKey("name")) system.Name = o.Value<string>("name").NullIfEmpty().BoundsCheckField(Limits.MaxSystemNameLength, "System name");
if (o.ContainsKey("description")) system.Description = o.Value<string>("description").NullIfEmpty().BoundsCheckField(Limits.MaxDescriptionLength, "System description");
if (o.ContainsKey("tag")) system.Tag = o.Value<string>("tag").NullIfEmpty().BoundsCheckField(Limits.MaxSystemTagLength, "System tag");
if (o.ContainsKey("avatar_url")) system.AvatarUrl = o.Value<string>("avatar_url").NullIfEmpty().BoundsCheckField(Limits.MaxUriLength, "System avatar URL");
if (o.ContainsKey("tz")) system.UiTz = o.Value<string>("tz") ?? "UTC";
if (o.ContainsKey("description_privacy")) system.DescriptionPrivacy = o.Value<string>("description_privacy").ParsePrivacy("description");
if (o.ContainsKey("member_list_privacy")) system.MemberListPrivacy = o.Value<string>("member_list_privacy").ParsePrivacy("member list");
if (o.ContainsKey("front_privacy")) system.FrontPrivacy = o.Value<string>("front_privacy").ParsePrivacy("front");
if (o.ContainsKey("front_history_privacy")) system.FrontHistoryPrivacy = o.Value<string>("front_history_privacy").ParsePrivacy("front history");
}
public static JObject ToJson(this PKMember member, LookupContext ctx)
{
var o = new JObject();
o.Add("id", member.Hid);
o.Add("name", member.Name);
o.Add("color", member.MemberPrivacy.CanAccess(ctx) ? member.Color : null);
o.Add("display_name", member.DisplayName);
o.Add("birthday", member.MemberPrivacy.CanAccess(ctx) && member.Birthday.HasValue ? DateTimeFormats.DateExportFormat.Format(member.Birthday.Value) : null);
o.Add("pronouns", member.MemberPrivacy.CanAccess(ctx) ? member.Pronouns : null);
o.Add("avatar_url", member.AvatarUrl);
o.Add("description", member.MemberPrivacy.CanAccess(ctx) ? member.Description : null);
o.Add("privacy", ctx == LookupContext.ByOwner ? (member.MemberPrivacy == PrivacyLevel.Private ? "private" : "public") : null);
var tagArray = new JArray();
foreach (var tag in member.ProxyTags)
tagArray.Add(new JObject {{"prefix", tag.Prefix}, {"suffix", tag.Suffix}});
o.Add("proxy_tags", tagArray);
o.Add("keep_proxy", member.KeepProxy);
o.Add("created", DateTimeFormats.TimestampExportFormat.Format(member.Created));
if (member.ProxyTags.Count > 0)
{
// Legacy compatibility only, TODO: remove at some point
o.Add("prefix", member.ProxyTags?.FirstOrDefault().Prefix);
o.Add("suffix", member.ProxyTags?.FirstOrDefault().Suffix);
}
return o;
}
public static void ApplyJson(this PKMember member, JObject o)
{
if (o.ContainsKey("name") && o["name"].Type == JTokenType.Null)
throw new JsonModelParseError("Member name can not be set to null.");
if (o.ContainsKey("name")) member.Name = o.Value<string>("name").BoundsCheckField(Limits.MaxMemberNameLength, "Member name");
if (o.ContainsKey("color")) member.Color = o.Value<string>("color").NullIfEmpty()?.ToLower();
if (o.ContainsKey("display_name")) member.DisplayName = o.Value<string>("display_name").NullIfEmpty().BoundsCheckField(Limits.MaxMemberNameLength, "Member display name");
if (o.ContainsKey("avatar_url")) member.AvatarUrl = o.Value<string>("avatar_url").NullIfEmpty().BoundsCheckField(Limits.MaxUriLength, "Member avatar URL");
if (o.ContainsKey("birthday"))
{
var str = o.Value<string>("birthday").NullIfEmpty();
var res = DateTimeFormats.DateExportFormat.Parse(str);
if (res.Success) member.Birthday = res.Value;
else if (str == null) member.Birthday = null;
else throw new JsonModelParseError("Could not parse member birthday.");
}
if (o.ContainsKey("pronouns")) member.Pronouns = o.Value<string>("pronouns").NullIfEmpty().BoundsCheckField(Limits.MaxPronounsLength, "Member pronouns");
if (o.ContainsKey("description")) member.Description = o.Value<string>("description").NullIfEmpty().BoundsCheckField(Limits.MaxDescriptionLength, "Member descriptoin");
if (o.ContainsKey("keep_proxy")) member.KeepProxy = o.Value<bool>("keep_proxy");
if (o.ContainsKey("prefix") || o.ContainsKey("suffix") && !o.ContainsKey("proxy_tags"))
member.ProxyTags = new[] {new ProxyTag(o.Value<string>("prefix"), o.Value<string>("suffix"))};
else if (o.ContainsKey("proxy_tags"))
{
member.ProxyTags = o.Value<JArray>("proxy_tags")
.OfType<JObject>().Select(o => new ProxyTag(o.Value<string>("prefix"), o.Value<string>("suffix")))
.ToList();
}
if (o.ContainsKey("privacy")) member.MemberPrivacy = o.Value<string>("privacy").ParsePrivacy("member");
}
private static string BoundsCheckField(this string input, int maxLength, string nameInError)
{
if (input != null && input.Length > maxLength)
throw new JsonModelParseError($"{nameInError} too long ({input.Length} > {maxLength}).");
return input;
}
private static string ToJsonString(this PrivacyLevel level) => level == PrivacyLevel.Private ? "private" : "public";
private static PrivacyLevel ParsePrivacy(this string input, string errorName)
{
if (input == null) return PrivacyLevel.Private;
if (input == "") return PrivacyLevel.Private;
if (input == "private") return PrivacyLevel.Private;
if (input == "public") return PrivacyLevel.Public;
throw new JsonModelParseError($"Could not parse {errorName} privacy.");
}
}
public class JsonModelParseError: Exception
{
public JsonModelParseError(string message): base(message) { }
}
}

View File

@@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
@@ -15,9 +16,9 @@ namespace PluralKit.API
public class MemberController: ControllerBase
{
private IDataStore _data;
private TokenAuthService _auth;
private IAuthorizationService _auth;
public MemberController(IDataStore data, TokenAuthService auth)
public MemberController(IDataStore data, IAuthorizationService auth)
{
_data = data;
_auth = auth;
@@ -29,15 +30,15 @@ namespace PluralKit.API
var member = await _data.GetMemberByHid(hid);
if (member == null) return NotFound("Member not found.");
return Ok(member.ToJson(_auth.ContextFor(member)));
return Ok(member.ToJson(User.ContextFor(member)));
}
[HttpPost]
[RequiresSystem]
[Authorize]
public async Task<ActionResult<JObject>> PostMember([FromBody] JObject properties)
{
var system = _auth.CurrentSystem;
var system = User.CurrentSystem();
if (!properties.ContainsKey("name"))
return BadRequest("Member name must be specified.");
@@ -57,17 +58,18 @@ namespace PluralKit.API
}
await _data.SaveMember(member);
return Ok(member.ToJson(_auth.ContextFor(member)));
return Ok(member.ToJson(User.ContextFor(member)));
}
[HttpPatch("{hid}")]
[RequiresSystem]
[Authorize]
public async Task<ActionResult<JObject>> PatchMember(string hid, [FromBody] JObject changes)
{
var member = await _data.GetMemberByHid(hid);
if (member == null) return NotFound("Member not found.");
if (member.System != _auth.CurrentSystem.Id) return Unauthorized($"Member '{hid}' is not part of your system.");
var res = await _auth.AuthorizeAsync(User, member, "EditMember");
if (!res.Succeeded) return Unauthorized($"Member '{hid}' is not part of your system.");
try
{
@@ -79,17 +81,18 @@ namespace PluralKit.API
}
await _data.SaveMember(member);
return Ok(member.ToJson(_auth.ContextFor(member)));
return Ok(member.ToJson(User.ContextFor(member)));
}
[HttpDelete("{hid}")]
[RequiresSystem]
[Authorize]
public async Task<ActionResult> DeleteMember(string hid)
{
var member = await _data.GetMemberByHid(hid);
if (member == null) return NotFound("Member not found.");
if (member.System != _auth.CurrentSystem.Id) return Unauthorized($"Member '{hid}' is not part of your system.");
var res = await _auth.AuthorizeAsync(User, member, "EditMember");
if (!res.Succeeded) return Unauthorized($"Member '{hid}' is not part of your system.");
await _data.DeleteMember(member);
return Ok();

View File

@@ -30,12 +30,10 @@ namespace PluralKit.API
public class MessageController: ControllerBase
{
private IDataStore _data;
private TokenAuthService _auth;
public MessageController(IDataStore _data, TokenAuthService auth)
public MessageController(IDataStore _data)
{
this._data = _data;
_auth = auth;
}
[HttpGet("{mid}")]
@@ -50,8 +48,8 @@ namespace PluralKit.API
Id = msg.Message.Mid.ToString(),
Channel = msg.Message.Channel.ToString(),
Sender = msg.Message.Sender.ToString(),
Member = msg.Member.ToJson(_auth.ContextFor(msg.System)),
System = msg.System.ToJson(_auth.ContextFor(msg.System)),
Member = msg.Member.ToJson(User.ContextFor(msg.System)),
System = msg.System.ToJson(User.ContextFor(msg.System)),
Original = msg.Message.OriginalMid?.ToString()
};
}

View File

@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Dapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -41,9 +42,9 @@ namespace PluralKit.API
{
private IDataStore _data;
private IDatabase _conn;
private TokenAuthService _auth;
private IAuthorizationService _auth;
public SystemController(IDataStore data, IDatabase conn, TokenAuthService auth)
public SystemController(IDataStore data, IDatabase conn, IAuthorizationService auth)
{
_data = data;
_conn = conn;
@@ -51,10 +52,11 @@ namespace PluralKit.API
}
[HttpGet]
[RequiresSystem]
public Task<ActionResult<JObject>> GetOwnSystem()
[Authorize]
public async Task<ActionResult<JObject>> GetOwnSystem()
{
return Task.FromResult<ActionResult<JObject>>(Ok(_auth.CurrentSystem.ToJson(_auth.ContextFor(_auth.CurrentSystem))));
var system = await _conn.Execute(c => c.QuerySystem(User.CurrentSystem()));
return system.ToJson(User.ContextFor(system));
}
[HttpGet("{hid}")]
@@ -62,7 +64,7 @@ namespace PluralKit.API
{
var system = await _data.GetSystemByHid(hid);
if (system == null) return NotFound("System not found.");
return Ok(system.ToJson(_auth.ContextFor(system)));
return Ok(system.ToJson(User.ContextFor(system)));
}
[HttpGet("{hid}/members")]
@@ -71,13 +73,13 @@ namespace PluralKit.API
var system = await _data.GetSystemByHid(hid);
if (system == null) return NotFound("System not found.");
if (!system.MemberListPrivacy.CanAccess(_auth.ContextFor(system)))
if (!system.MemberListPrivacy.CanAccess(User.ContextFor(system)))
return StatusCode(StatusCodes.Status403Forbidden, "Unauthorized to view member list.");
var members = _data.GetSystemMembers(system);
return Ok(await members
.Where(m => m.MemberPrivacy.CanAccess(_auth.ContextFor(system)))
.Select(m => m.ToJson(_auth.ContextFor(system)))
.Where(m => m.MemberPrivacy.CanAccess(User.ContextFor(system)))
.Select(m => m.ToJson(User.ContextFor(system)))
.ToListAsync());
}
@@ -88,9 +90,9 @@ namespace PluralKit.API
var system = await _data.GetSystemByHid(hid);
if (system == null) return NotFound("System not found.");
if (!system.FrontHistoryPrivacy.CanAccess(_auth.ContextFor(system)))
return StatusCode(StatusCodes.Status403Forbidden, "Unauthorized to view front history.");
var auth = await _auth.AuthorizeAsync(User, system, "ViewFrontHistory");
if (!auth.Succeeded) return StatusCode(StatusCodes.Status403Forbidden, "Unauthorized to view front history.");
using (var conn = await _conn.Obtain())
{
@@ -112,26 +114,25 @@ namespace PluralKit.API
var system = await _data.GetSystemByHid(hid);
if (system == null) return NotFound("System not found.");
if (!system.FrontPrivacy.CanAccess(_auth.ContextFor(system)))
return StatusCode(StatusCodes.Status403Forbidden, "Unauthorized to view fronter.");
var auth = await _auth.AuthorizeAsync(User, system, "ViewFront");
if (!auth.Succeeded) return StatusCode(StatusCodes.Status403Forbidden, "Unauthorized to view fronter.");
var sw = await _data.GetLatestSwitch(system);
var sw = await _data.GetLatestSwitch(system.Id);
if (sw == null) return NotFound("System has no registered switches.");
var members = _data.GetSwitchMembers(sw);
return Ok(new FrontersReturn
{
Timestamp = sw.Timestamp,
Members = await members.Select(m => m.ToJson(_auth.ContextFor(system))).ToListAsync()
Members = await members.Select(m => m.ToJson(User.ContextFor(system))).ToListAsync()
});
}
[HttpPatch]
[RequiresSystem]
[Authorize]
public async Task<ActionResult<JObject>> EditSystem([FromBody] JObject changes)
{
var system = _auth.CurrentSystem;
var system = await _conn.Execute(c => c.QuerySystem(User.CurrentSystem()));
try
{
system.ApplyJson(changes);
@@ -142,18 +143,18 @@ namespace PluralKit.API
}
await _data.SaveSystem(system);
return Ok(system.ToJson(_auth.ContextFor(system)));
return Ok(system.ToJson(User.ContextFor(system)));
}
[HttpPost("switches")]
[RequiresSystem]
[Authorize]
public async Task<IActionResult> PostSwitch([FromBody] PostSwitchParams param)
{
if (param.Members.Distinct().Count() != param.Members.Count())
if (param.Members.Distinct().Count() != param.Members.Count)
return BadRequest("Duplicate members in member list.");
// We get the current switch, if it exists
var latestSwitch = await _data.GetLatestSwitch(_auth.CurrentSystem);
var latestSwitch = await _data.GetLatestSwitch(User.CurrentSystem());
if (latestSwitch != null)
{
var latestSwitchMembers = _data.GetSwitchMembers(latestSwitch);
@@ -169,7 +170,7 @@ namespace PluralKit.API
membersList = (await conn.QueryAsync<PKMember>("select * from members where hid = any(@Hids)", new {Hids = param.Members})).ToList();
foreach (var member in membersList)
if (member.System != _auth.CurrentSystem.Id)
if (member.System != User.CurrentSystem())
return BadRequest($"Cannot switch to member '{member.Hid}' not in system.");
// membersList is in DB order, and we want it in actual input order
@@ -185,7 +186,7 @@ namespace PluralKit.API
}
// Finally, log the switch (yay!)
await _data.AddSwitch(_auth.CurrentSystem, membersInOrder);
await _data.AddSwitch(User.CurrentSystem(), membersInOrder);
return NoContent();
}
}