Skip to content

Commit 0185557

Browse files
committed
way too much stuff for a single commit
1 parent 4e4f456 commit 0185557

37 files changed

+1109
-190
lines changed

rubberduckvba.Server/Api/Admin/AdminController.cs

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public class AdminController(ConfigurationOptions options, HangfireLauncherServi
1616
/// Enqueues a job that updates xmldoc content from the latest release/pre-release tags.
1717
/// </summary>
1818
/// <returns>The unique identifier of the enqueued job.</returns>
19-
[Authorize("github", Roles = RDConstants.AdminRole)]
19+
[Authorize("github", Roles = RDConstants.Roles.AdminRole)]
2020
[HttpPost("admin/update/xmldoc")]
2121
public IActionResult UpdateXmldocContent()
2222
{
@@ -28,43 +28,96 @@ public IActionResult UpdateXmldocContent()
2828
/// Enqueues a job that gets the latest release/pre-release tags and their respective assets, and updates the installer download stats.
2929
/// </summary>
3030
/// <returns>The unique identifier of the enqueued job.</returns>
31-
[Authorize("github", Roles = RDConstants.AdminRole)]
31+
[Authorize("github", Roles = RDConstants.Roles.AdminRole)]
3232
[HttpPost("admin/update/tags")]
3333
public IActionResult UpdateTagMetadata()
3434
{
3535
var jobId = hangfire.UpdateTagMetadata();
3636
return Ok(jobId);
3737
}
3838

39-
[Authorize("github", Roles = RDConstants.AdminRole)]
39+
[Authorize("github", Roles = RDConstants.Roles.AdminRole)]
4040
[HttpPost("admin/cache/clear")]
4141
public IActionResult ClearCache()
4242
{
4343
cache.Clear();
4444
return Ok();
4545
}
4646

47-
[Authorize("github", Roles = $"{RDConstants.AdminRole},{RDConstants.ReviewerRole}")]
47+
[Authorize("github", Roles = $"{RDConstants.Roles.AdminRole},{RDConstants.Roles.ReviewerRole},{RDConstants.Roles.WriterRole}")]
4848
[HttpGet("admin/audits/pending")]
4949
public async Task<IActionResult> GetPendingAudits()
5050
{
51-
var edits = await audits.GetPendingItems<FeatureEditEntity>();
52-
var ops = await audits.GetPendingItems<FeatureOpEntity>();
51+
var edits = await audits.GetPendingItems<FeatureEditViewEntity>(User.Identity);
52+
var ops = await audits.GetPendingItems<FeatureOpEntity>(User.Identity);
5353

5454
return Ok(new { edits = edits.ToArray(), other = ops.ToArray() });
5555
}
5656

57-
[Authorize("github", Roles = $"{RDConstants.AdminRole},{RDConstants.ReviewerRole}")]
58-
[HttpGet("admin/audits/{featureId}")]
57+
[Authorize("github", Roles = $"{RDConstants.Roles.AdminRole},{RDConstants.Roles.ReviewerRole},{RDConstants.Roles.WriterRole}")]
58+
[HttpGet("profile/activity")]
59+
public async Task<IActionResult> GetUserActivity()
60+
{
61+
if (User.Identity is not IIdentity identity)
62+
{
63+
// this is arguably a bug in the authentication middleware, but we can handle it gracefully here
64+
return Unauthorized("User identity is not available.");
65+
}
66+
67+
var activity = await audits.GetAllActivity(identity);
68+
return Ok(activity);
69+
}
70+
71+
private static readonly AuditActivityType[] EditActivityTypes = [
72+
AuditActivityType.SubmitEdit,
73+
AuditActivityType.ApproveEdit,
74+
AuditActivityType.RejectEdit
75+
];
76+
77+
private static readonly AuditActivityType[] OpActivityTypes = [
78+
AuditActivityType.SubmitCreate,
79+
AuditActivityType.ApproveCreate,
80+
AuditActivityType.RejectCreate,
81+
AuditActivityType.SubmitDelete,
82+
AuditActivityType.ApproveDelete,
83+
AuditActivityType.RejectDelete
84+
];
85+
86+
[Authorize("github", Roles = $"{RDConstants.Roles.AdminRole},{RDConstants.Roles.ReviewerRole}")]
87+
[HttpGet("admin/audits/{id}")]
88+
public async Task<IActionResult> GetAudit([FromRoute] int id, [FromQuery] string type)
89+
{
90+
if (!Enum.TryParse<AuditActivityType>(type, ignoreCase: true, out var validType))
91+
{
92+
return BadRequest("Invalid activity type.");
93+
}
94+
95+
var edit = (FeatureEditViewEntity?)null;
96+
var op = (FeatureOpEntity?)null;
97+
98+
if (EditActivityTypes.Contains(validType))
99+
{
100+
edit = await audits.GetItem<FeatureEditViewEntity>(id);
101+
}
102+
else if (OpActivityTypes.Contains(validType))
103+
{
104+
op = await audits.GetItem<FeatureOpEntity>(id);
105+
}
106+
107+
return Ok(new { edits = new[] { edit }, other = op is null ? [] : new[] { op } });
108+
}
109+
110+
[Authorize("github", Roles = $"{RDConstants.Roles.AdminRole},{RDConstants.Roles.ReviewerRole}")]
111+
[HttpGet("admin/audits/feature/{featureId}")]
59112
public async Task<IActionResult> GetPendingAudits([FromRoute] int featureId)
60113
{
61-
var edits = await audits.GetPendingItems<FeatureEditEntity>(featureId);
62-
var ops = await audits.GetPendingItems<FeatureOpEntity>(featureId);
114+
var edits = await audits.GetPendingItems<FeatureEditEntity>(User.Identity, featureId);
115+
var ops = await audits.GetPendingItems<FeatureOpEntity>(User.Identity, featureId);
63116

64117
return Ok(new { edits = edits.ToArray(), other = ops.ToArray() });
65118
}
66119

67-
[Authorize("github", Roles = $"{RDConstants.AdminRole},{RDConstants.ReviewerRole}")]
120+
[Authorize("github", Roles = $"{RDConstants.Roles.AdminRole},{RDConstants.Roles.ReviewerRole}")]
68121
[HttpPost("admin/audits/approve/{id}")]
69122
public async Task<IActionResult> ApprovePendingAudit([FromRoute] int id)
70123
{
@@ -74,13 +127,13 @@ public async Task<IActionResult> ApprovePendingAudit([FromRoute] int id)
74127
return Unauthorized("User identity is not available.");
75128
}
76129

77-
var edits = await audits.GetPendingItems<FeatureEditEntity>();
130+
var edits = await audits.GetPendingItems<FeatureEditEntity>(User.Identity);
78131
AuditEntity? audit;
79132

80133
audit = edits.SingleOrDefault(e => e.Id == id);
81134
if (audit is null)
82135
{
83-
var ops = await audits.GetPendingItems<FeatureOpEntity>();
136+
var ops = await audits.GetPendingItems<FeatureOpEntity>(User.Identity);
84137
audit = ops.SingleOrDefault(e => e.Id == id);
85138
}
86139

@@ -100,7 +153,7 @@ public async Task<IActionResult> ApprovePendingAudit([FromRoute] int id)
100153
return Ok("Operation was approved successfully.");
101154
}
102155

103-
[Authorize("github", Roles = $"{RDConstants.AdminRole},{RDConstants.ReviewerRole}")]
156+
[Authorize("github", Roles = $"{RDConstants.Roles.AdminRole},{RDConstants.Roles.ReviewerRole}")]
104157
[HttpPost("admin/audits/reject/{id}")]
105158
public async Task<IActionResult> RejectPendingAudit([FromRoute] int id)
106159
{
@@ -110,13 +163,13 @@ public async Task<IActionResult> RejectPendingAudit([FromRoute] int id)
110163
return Unauthorized("User identity is not available.");
111164
}
112165

113-
var edits = await audits.GetPendingItems<FeatureEditEntity>();
166+
var edits = await audits.GetPendingItems<FeatureEditEntity>(User.Identity);
114167
AuditEntity? audit;
115168

116169
audit = edits.SingleOrDefault(e => e.Id == id);
117170
if (audit is null)
118171
{
119-
var ops = await audits.GetPendingItems<FeatureOpEntity>();
172+
var ops = await audits.GetPendingItems<FeatureOpEntity>(User.Identity);
120173
audit = ops.SingleOrDefault(e => e.Id == id);
121174
}
122175

rubberduckvba.Server/Api/Auth/AuthController.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ public IActionResult Index()
6161
{
6262
Name = name,
6363
IsAuthenticated = isAuthenticated,
64-
IsAdmin = role == RDConstants.AdminRole,
65-
IsReviewer = role == RDConstants.AdminRole || role == RDConstants.ReviewerRole,
66-
IsWriter = role == RDConstants.WriterRole || role == RDConstants.AdminRole || role == RDConstants.ReviewerRole,
64+
IsAdmin = role == RDConstants.Roles.AdminRole,
65+
IsReviewer = role == RDConstants.Roles.AdminRole || role == RDConstants.Roles.ReviewerRole,
66+
IsWriter = role == RDConstants.Roles.WriterRole || role == RDConstants.Roles.AdminRole || role == RDConstants.Roles.ReviewerRole,
6767
};
6868

6969
return Ok(model);

rubberduckvba.Server/Api/Auth/ClaimsPrincipalExtensions.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.IdentityModel.Tokens;
22
using System.IdentityModel.Tokens.Jwt;
33
using System.Security.Claims;
4+
using System.Security.Principal;
45
using System.Text;
56

67
namespace rubberduckvba.Server.Api.Auth;
@@ -18,4 +19,54 @@ public static string AsJWT(this ClaimsPrincipal principal, string secret, string
1819

1920
return new JwtSecurityTokenHandler().WriteToken(token);
2021
}
22+
23+
/// <summary>
24+
/// <c>true</c> if the user is authenticated and has the <c>rd-admin</c> role.
25+
/// </summary>
26+
public static bool IsAdmin(this ClaimsPrincipal principal)
27+
{
28+
return principal.IsInRole(RDConstants.Roles.AdminRole);
29+
}
30+
31+
/// <summary>
32+
/// <c>true</c> if the user is authenticated and has the <c>rd-reviewer</c> or <c>rd-admin</c> role.
33+
/// </summary>
34+
public static bool IsReviewer(this ClaimsPrincipal principal)
35+
{
36+
return principal.IsInRole(RDConstants.Roles.ReviewerRole);
37+
}
2138
}
39+
40+
public static class ClaimsIdentityExtensions
41+
{
42+
/// <summary>
43+
/// <c>true</c> if the user is authenticated and has the <c>rd-admin</c> role.
44+
/// </summary>
45+
public static bool IsAdmin(this IIdentity identity)
46+
{
47+
return identity is ClaimsIdentity claimsIdentity && claimsIdentity.IsAdmin();
48+
}
49+
50+
/// <summary>
51+
/// <c>true</c> if the user is authenticated and has the <c>rd-reviewer</c> or <c>rd-admin</c> role.
52+
/// </summary>
53+
public static bool IsReviewer(this IIdentity identity)
54+
{
55+
return identity is ClaimsIdentity claimsIdentity && claimsIdentity.IsReviewer();
56+
}
57+
58+
/// <summary>
59+
/// <c>true</c> if the user is authenticated and has the <c>rd-admin</c> role.
60+
/// </summary>
61+
public static bool IsAdmin(this ClaimsIdentity identity)
62+
{
63+
return identity.IsAuthenticated && identity.HasClaim(ClaimTypes.Role, RDConstants.Roles.AdminRole);
64+
}
65+
/// <summary>
66+
/// <c>true</c> if the user is authenticated and has the <c>rd-reviewer</c> or <c>rd-admin</c> role.
67+
/// </summary>
68+
public static bool IsReviewer(this ClaimsIdentity identity)
69+
{
70+
return identity != null && identity.IsAuthenticated && (identity.HasClaim(ClaimTypes.Role, RDConstants.Roles.AdminRole) || identity.HasClaim(ClaimTypes.Role, RDConstants.Roles.ReviewerRole));
71+
}
72+
}

rubberduckvba.Server/Api/Features/FeatureViewModel.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,6 @@ public class InspectionsFeatureViewModel : FeatureViewModel
242242
public InspectionsFeatureViewModel(FeatureGraph model, IEnumerable<QuickFixViewModel> quickFixes, IDictionary<int, Tag> tagsByAssetId, bool summaryOnly = false)
243243
: base(model, summaryOnly)
244244
{
245-
246245
Inspections = model.Inspections.OrderBy(e => e.Name).Select(e => new InspectionViewModel(e, quickFixes, tagsByAssetId)).ToArray();
247246
}
248247

rubberduckvba.Server/Api/Features/FeaturesController.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ public IActionResult QuickFix([FromRoute] string name)
152152
}
153153

154154
[HttpGet("features/create")]
155-
[Authorize("github", Roles = $"{RDConstants.AdminRole},{RDConstants.ReviewerRole},{RDConstants.WriterRole}")]
155+
[Authorize("github", Roles = $"{RDConstants.Roles.AdminRole},{RDConstants.Roles.ReviewerRole}")]
156156
public async Task<ActionResult<FeatureEditViewModel>> Create([FromQuery] RepositoryId repository = RepositoryId.Rubberduck, [FromQuery] int? parentId = default)
157157
{
158158
var features = await GetFeatureOptions(repository);
@@ -163,7 +163,7 @@ public async Task<ActionResult<FeatureEditViewModel>> Create([FromQuery] Reposit
163163
}
164164

165165
[HttpPost("features/create")]
166-
[Authorize("github", Roles = $"{RDConstants.AdminRole},{RDConstants.ReviewerRole},{RDConstants.WriterRole}")]
166+
[Authorize("github", Roles = $"{RDConstants.Roles.AdminRole},{RDConstants.Roles.ReviewerRole}")]
167167
public async Task<ActionResult<FeatureEditViewModel>> Create([FromBody] FeatureEditViewModel model)
168168
{
169169
if (model.Id.HasValue || string.IsNullOrWhiteSpace(model.Name) || model.Name.Trim().Length < 3)
@@ -190,7 +190,7 @@ public async Task<ActionResult<FeatureEditViewModel>> Create([FromBody] FeatureE
190190
}
191191

192192
[HttpPost("features/update")]
193-
[Authorize("github", Roles = $"{RDConstants.AdminRole},{RDConstants.ReviewerRole},{RDConstants.WriterRole}")]
193+
[Authorize("github", Roles = $"{RDConstants.Roles.AdminRole},{RDConstants.Roles.ReviewerRole}")]
194194
public async Task<ActionResult<FeatureEditViewModel>> Update([FromBody] FeatureEditViewModel model)
195195
{
196196
if (model.Id.GetValueOrDefault() == default)
@@ -217,7 +217,7 @@ public async Task<ActionResult<FeatureEditViewModel>> Update([FromBody] FeatureE
217217
}
218218

219219
[HttpPost("features/delete")]
220-
[Authorize("github", Roles = $"{RDConstants.AdminRole},{RDConstants.ReviewerRole},{RDConstants.WriterRole}")]
220+
[Authorize("github", Roles = $"{RDConstants.Roles.AdminRole},{RDConstants.Roles.ReviewerRole}")]
221221
public async Task Delete([FromBody] Feature model)
222222
{
223223
if (model.Id == default)

rubberduckvba.Server/GitHubAuthenticationHandler.cs

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Microsoft.Extensions.Caching.Memory;
33
using Microsoft.Extensions.Options;
44
using rubberduckvba.Server.Services;
5+
using System.Collections.Concurrent;
56
using System.Security.Claims;
67
using System.Text.Encodings.Web;
78

@@ -11,8 +12,6 @@ public class GitHubAuthenticationHandler : AuthenticationHandler<AuthenticationS
1112
{
1213
public static readonly string AuthCookie = "x-access-token";
1314

14-
private static readonly object _lock = new object();
15-
1615
private readonly IGitHubClientService _github;
1716
private readonly IMemoryCache _cache;
1817

@@ -29,6 +28,8 @@ public GitHubAuthenticationHandler(IGitHubClientService github, IMemoryCache cac
2928
_cache = cache;
3029
}
3130

31+
private static readonly ConcurrentDictionary<string, Task<AuthenticateResult?>> _authApiTask = new();
32+
3233
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
3334
{
3435
try
@@ -46,20 +47,17 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
4647
return Task.FromResult(cachedResult)!;
4748
}
4849

49-
lock (_lock)
50+
if (TryAuthenticateGitHubToken(token, out var result)
51+
&& result is AuthenticateResult
52+
&& result.Ticket is AuthenticationTicket ticket)
5053
{
51-
if (TryAuthenticateGitHubToken(token, out var result)
52-
&& result is AuthenticateResult
53-
&& result.Ticket is AuthenticationTicket ticket)
54-
{
55-
CacheAuthenticatedTicket(token, ticket);
56-
return Task.FromResult(result!);
57-
}
58-
59-
if (TryAuthenticateFromCache(token, out cachedResult))
60-
{
61-
return Task.FromResult(cachedResult!);
62-
}
54+
CacheAuthenticatedTicket(token, ticket);
55+
return Task.FromResult(result!);
56+
}
57+
58+
if (TryAuthenticateFromCache(token, out cachedResult))
59+
{
60+
return Task.FromResult(cachedResult!);
6361
}
6462

6563
return Task.FromResult(AuthenticateResult.Fail("Missing or invalid access token"));
@@ -99,16 +97,30 @@ private bool TryAuthenticateFromCache(string token, out AuthenticateResult? resu
9997
private bool TryAuthenticateGitHubToken(string token, out AuthenticateResult? result)
10098
{
10199
result = null;
102-
var principal = _github.ValidateTokenAsync(token).GetAwaiter().GetResult();
100+
if (_authApiTask.TryGetValue(token, out var task) && task is not null)
101+
{
102+
result = task.GetAwaiter().GetResult();
103+
return result is not null;
104+
}
105+
106+
_authApiTask[token] = AuthenticateGitHubAsync(token);
107+
result = _authApiTask[token].GetAwaiter().GetResult();
108+
109+
_authApiTask[token] = null!;
110+
return result is not null;
111+
}
112+
113+
private async Task<AuthenticateResult?> AuthenticateGitHubAsync(string token)
114+
{
115+
var principal = await _github.ValidateTokenAsync(token);
103116
if (principal is ClaimsPrincipal)
104117
{
105118
Context.User = principal;
106119
Thread.CurrentPrincipal = principal;
107120

108121
var ticket = new AuthenticationTicket(principal, "github");
109-
result = AuthenticateResult.Success(ticket);
110-
return true;
122+
return AuthenticateResult.Success(ticket);
111123
}
112-
return false;
124+
return null;
113125
}
114126
}

0 commit comments

Comments
 (0)