Skip to content

Commit 4e4f456

Browse files
committed
added auditbox components, tweak auth, add roles
1 parent 76a48d3 commit 4e4f456

20 files changed

+501
-117
lines changed

rubberduckvba.Server/Api/Admin/AdminController.cs

Lines changed: 7 additions & 7 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")]
19+
[Authorize("github", Roles = RDConstants.AdminRole)]
2020
[HttpPost("admin/update/xmldoc")]
2121
public IActionResult UpdateXmldocContent()
2222
{
@@ -28,23 +28,23 @@ 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")]
31+
[Authorize("github", Roles = RDConstants.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")]
39+
[Authorize("github", Roles = RDConstants.AdminRole)]
4040
[HttpPost("admin/cache/clear")]
4141
public IActionResult ClearCache()
4242
{
4343
cache.Clear();
4444
return Ok();
4545
}
4646

47-
[Authorize("github")]
47+
[Authorize("github", Roles = $"{RDConstants.AdminRole},{RDConstants.ReviewerRole}")]
4848
[HttpGet("admin/audits/pending")]
4949
public async Task<IActionResult> GetPendingAudits()
5050
{
@@ -54,7 +54,7 @@ public async Task<IActionResult> GetPendingAudits()
5454
return Ok(new { edits = edits.ToArray(), other = ops.ToArray() });
5555
}
5656

57-
[Authorize("github")]
57+
[Authorize("github", Roles = $"{RDConstants.AdminRole},{RDConstants.ReviewerRole}")]
5858
[HttpGet("admin/audits/{featureId}")]
5959
public async Task<IActionResult> GetPendingAudits([FromRoute] int featureId)
6060
{
@@ -64,7 +64,7 @@ public async Task<IActionResult> GetPendingAudits([FromRoute] int featureId)
6464
return Ok(new { edits = edits.ToArray(), other = ops.ToArray() });
6565
}
6666

67-
[Authorize("github")]
67+
[Authorize("github", Roles = $"{RDConstants.AdminRole},{RDConstants.ReviewerRole}")]
6868
[HttpPost("admin/audits/approve/{id}")]
6969
public async Task<IActionResult> ApprovePendingAudit([FromRoute] int id)
7070
{
@@ -100,7 +100,7 @@ public async Task<IActionResult> ApprovePendingAudit([FromRoute] int id)
100100
return Ok("Operation was approved successfully.");
101101
}
102102

103-
[Authorize("github")]
103+
[Authorize("github", Roles = $"{RDConstants.AdminRole},{RDConstants.ReviewerRole}")]
104104
[HttpPost("admin/audits/reject/{id}")]
105105
public async Task<IActionResult> RejectPendingAudit([FromRoute] int id)
106106
{

rubberduckvba.Server/Api/Auth/AuthController.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ namespace rubberduckvba.Server.Api.Auth;
1010

1111
public record class UserViewModel
1212
{
13-
public static UserViewModel Anonymous { get; } = new UserViewModel { Name = "(anonymous)", IsAuthenticated = false, IsAdmin = false };
13+
public static UserViewModel Anonymous { get; } = new UserViewModel { Name = "(anonymous)", IsAuthenticated = false, IsAdmin = false, IsReviewer = false, IsWriter = false };
1414

1515
public string Name { get; init; } = default!;
1616
public bool IsAuthenticated { get; init; }
1717
public bool IsAdmin { get; init; }
18+
public bool IsReviewer { get; init; }
19+
public bool IsWriter { get; init; }
1820
}
1921

2022
public record class SignInViewModel
@@ -59,7 +61,9 @@ public IActionResult Index()
5961
{
6062
Name = name,
6163
IsAuthenticated = isAuthenticated,
62-
IsAdmin = role == configuration.Value.OwnerOrg
64+
IsAdmin = role == RDConstants.AdminRole,
65+
IsReviewer = role == RDConstants.AdminRole || role == RDConstants.ReviewerRole,
66+
IsWriter = role == RDConstants.WriterRole || role == RDConstants.AdminRole || role == RDConstants.ReviewerRole,
6367
};
6468

6569
return Ok(model);

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")]
155+
[Authorize("github", Roles = $"{RDConstants.AdminRole},{RDConstants.ReviewerRole},{RDConstants.WriterRole}")]
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")]
166+
[Authorize("github", Roles = $"{RDConstants.AdminRole},{RDConstants.ReviewerRole},{RDConstants.WriterRole}")]
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")]
193+
[Authorize("github", Roles = $"{RDConstants.AdminRole},{RDConstants.ReviewerRole},{RDConstants.WriterRole}")]
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")]
220+
[Authorize("github", Roles = $"{RDConstants.AdminRole},{RDConstants.ReviewerRole},{RDConstants.WriterRole}")]
221221
public async Task Delete([FromBody] Feature model)
222222
{
223223
if (model.Id == default)

rubberduckvba.Server/GitHubSettings.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ public record class ConnectionSettings
1313
public string HangfireDb { get; set; } = default!;
1414
}
1515

16+
public static class RDConstants
17+
{
18+
public const int OrganisationId = 12832254;
19+
public const string WebAdminTeam = "WebAdmin";
20+
public const string ContributorsTeam = "Contributors";
21+
22+
public const string ReaderRole = "rd-reader";
23+
public const string WriterRole = "rd-writer";
24+
public const string ReviewerRole = "rd-reviewer";
25+
public const string AdminRole = "rd-admin";
26+
}
27+
1628
public record class GitHubSettings
1729
{
1830
public string ClientId { get; set; } = default!;

rubberduckvba.Server/Services/GitHubClientService.cs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,46 @@ private class ReleaseComparer : IEqualityComparer<Release>
4242
var credentials = new Credentials(token);
4343
var client = new GitHubClient(new ProductHeaderValue(config.UserAgent), new InMemoryCredentialStore(credentials));
4444

45+
var user = await client.User.Current();
4546
var orgs = await client.Organization.GetAllForCurrent();
46-
var isOrgMember = orgs.Any(e => e.Id == config.RubberduckOrgId);
47-
if (!isOrgMember)
47+
48+
var org = orgs.SingleOrDefault(e => e.Id == RDConstants.OrganisationId);
49+
var isOrgMember = org is Organization rdOrg;
50+
51+
var claims = new List<Claim>
4852
{
49-
return null;
50-
}
53+
new(ClaimTypes.Name, user.Login),
54+
new(ClaimTypes.Authentication, token),
55+
new("access_token", token)
56+
};
5157

52-
var user = await client.User.Current();
53-
var identity = new ClaimsIdentity(new[]
58+
if (isOrgMember && !user.Suspended)
5459
{
55-
new Claim(ClaimTypes.Name, user.Login),
56-
new Claim(ClaimTypes.Role, config.OwnerOrg),
57-
new Claim(ClaimTypes.Authentication, token),
58-
new Claim("access_token", token)
59-
}, "github");
60+
var teams = await client.Organization.Team.GetAllForCurrent();
61+
62+
var adminTeam = teams.SingleOrDefault(e => e.Name == RDConstants.WebAdminTeam);
63+
if (adminTeam is not null)
64+
{
65+
// authenticated members of the org who are in the admin team can manage the site and approve their own changes
66+
claims.Add(new Claim(ClaimTypes.Role, RDConstants.AdminRole));
67+
}
68+
else
69+
{
70+
var contributorsTeam = teams.SingleOrDefault(e => e.Name == RDConstants.ContributorsTeam);
71+
if (contributorsTeam is not null)
72+
{
73+
// members of the contributors team can review/approve/reject suggested changes
74+
claims.Add(new Claim(ClaimTypes.Role, RDConstants.ReviewerRole));
75+
}
76+
else
77+
{
78+
// authenticated members of the org can submit edits
79+
claims.Add(new Claim(ClaimTypes.Role, RDConstants.WriterRole));
80+
}
81+
}
82+
}
83+
84+
var identity = new ClaimsIdentity(claims, "github");
6085
return new ClaimsPrincipal(identity);
6186
}
6287

rubberduckvba.client/rubberduckvba.client.esproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
<BuildOutputFolder>$(MSBuildProjectDirectory)\dist\rubberduckvba.client\</BuildOutputFolder>
99
</PropertyGroup>
1010
<ItemGroup>
11+
<Folder Include="src\app\components\audits\feature-state.review\" />
12+
<Folder Include="src\app\components\audits\feature-delete.review\" />
1113
<Folder Include="src\app\routes\build\" />
1214
</ItemGroup>
1315
</Project>

rubberduckvba.client/src/app/app.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, OnInit } from '@angular/core';
1+
import { Component } from '@angular/core';
22

33
@Component({
44
selector: 'app-root',

rubberduckvba.client/src/app/app.module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ import { DefaultUrlSerializer, UrlTree } from '@angular/router';
4141
import { AuthComponent } from './routes/auth/auth.component';
4242
import { AuthMenuComponent } from './components/auth-menu/auth-menu.component';
4343
import { AuditsAdminComponent } from './routes/audits/audits.component';
44+
import { AuditFeatureAdditionComponent } from './components/audits/feature-add.review/feature-add.review.component';
45+
import { AuditBoxComponent } from './components/audits/audit-box/audit-box.component';
46+
import { AuditFeatureEditMarkdownComponent } from './components/audits/feature-markdown.review/feature-edit-markdown.review.component';
4447

4548
/**
4649
* https://stackoverflow.com/a/39560520
@@ -63,6 +66,9 @@ export class LowerCaseUrlSerializer extends DefaultUrlSerializer {
6366
AuthComponent,
6467
AuthMenuComponent,
6568
AuditsAdminComponent,
69+
AuditBoxComponent,
70+
AuditFeatureAdditionComponent,
71+
AuditFeatureEditMarkdownComponent,
6672
IndenterComponent,
6773
FeaturesComponent,
6874
FeatureComponent,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<div class="card">
2+
<div class="card-header">
3+
<div *ngIf="isEdit"><h4>{{auditEdit!.fieldName}}</h4></div>
4+
<div *ngIf="isCreateOp"><h4>Create</h4><span>{{auditOp!.name}}</span></div>
5+
<div *ngIf="isDeleteOp"><h4>Delete</h4><span>{{auditOp!.name}}</span></div>
6+
<div>Submitted <strong>{{dateSubmitted}}</strong> by <strong>{{author}}</strong></div>
7+
<div class="small text-muted" *ngIf="dateModified.length > 0">, last modified <strong>{{dateModified}}</strong></div>
8+
</div>
9+
<div class="card-body">
10+
<div *ngIf="isCreateOp">
11+
<review-feature-add [audit]="auditOp"></review-feature-add>
12+
</div>
13+
<div *ngIf="isDeleteOp">
14+
<p>(todo: delete op)</p>
15+
<!-- review-feature-delete [audit]="audit" -->
16+
</div>
17+
<div *ngIf="isEdit">
18+
<review-edit-markdown [audit]="auditEdit"></review-edit-markdown>
19+
</div>
20+
</div>
21+
<div class="card-footer">
22+
<div class="text-end">
23+
<button type="button" class="btn btn-outline-danger" data-toggle="modal" data-target="#confirmReject"><fa-icon [icon]="['fas','trash-can']"></fa-icon>&nbsp;Reject</button>
24+
<button type="button" class="btn btn-outline-success" data-toggle="modal" data-target="#confirmApprove"><fa-icon [icon]="['fas','check']"></fa-icon>&nbsp;Approve</button>
25+
</div>
26+
</div>
27+
</div>
28+
29+
<div id="confirmApprove" class="modal" role="dialog" aria-hidden="true">
30+
<div class="modal-content" *ngIf="audit">
31+
<div class="modal-header align-content-center">
32+
<h4><img src="../../assets/vector-ducky-540.png" height="32">&nbsp;Approve this operation</h4>
33+
<button type="button" class="btn-close" aria-label="close" data-dismiss="modal"></button>
34+
</div>
35+
<div class="modal-body my-2 mx-4">
36+
<div class="row">
37+
<p>Audit record ID <strong>{{audit.id}}</strong> will be marked as approved. Proceed?</p>
38+
<hr />
39+
<p class="small text-muted"><fa-icon [icon]="'triangle-exclamation'"></fa-icon> This action will be logged and associated to your login.</p>
40+
</div>
41+
</div>
42+
<div class="modal-footer">
43+
<button type="button" class="btn btn-outline-primary" data-dismiss="modal">Cancel</button>
44+
<button type="button" class="btn btn-outline-success" (click)="onConfirmApprove()">Confirm</button>
45+
</div>
46+
</div>
47+
</div>
48+
49+
<div id="confirmReject" class="modal" role="dialog" aria-hidden="true">
50+
<div class="modal-content" *ngIf="audit">
51+
<div class="modal-header align-content-center">
52+
<h4><img src="../../assets/vector-ducky-540.png" height="32">&nbsp;Reject this operation</h4>
53+
<button type="button" class="btn-close" aria-label="close" data-dismiss="modal"></button>
54+
</div>
55+
<div class="modal-body my-2 mx-4">
56+
<div class="row">
57+
<p>Audit record ID <strong>{{audit.id}}</strong> will be marked as approved. Proceed?</p>
58+
<hr />
59+
<p class="small text-muted"><fa-icon [icon]="'triangle-exclamation'"></fa-icon> This action will be logged and associated to your login.</p>
60+
</div>
61+
</div>
62+
<div class="modal-footer">
63+
<button type="button" class="btn btn-outline-primary" data-dismiss="modal">Cancel</button>
64+
<button type="button" class="btn btn-outline-success" (click)="onConfirmReject()">Confirm</button>
65+
</div>
66+
</div>
67+
</div>

0 commit comments

Comments
 (0)