diff --git a/src/Konmaripo.Web.Tests.Unit/QuickLinqTest.cs b/src/Konmaripo.Web.Tests.Unit/QuickLinqTest.cs new file mode 100644 index 0000000..ace054c --- /dev/null +++ b/src/Konmaripo.Web.Tests.Unit/QuickLinqTest.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace Konmaripo.Web.Tests.Unit +{ + public class QuickLinqTest + { + [Fact] + public void LinqSequenceStuff() + { + var fullList = new List { 1, 2, 3, 4, 5 }; + var firstExcept = new List { 1, 2}; + var secondExcept = new List { 5 }; + + var result = fullList.Except(firstExcept).Except(secondExcept); + + result.Count().Should().Be(2); + result.Should().NotContain(1); + result.Should().NotContain(2); + result.Should().Contain(3); + result.Should().Contain(4); + result.Should().NotContain(5); + + result.Should().BeEquivalentTo(new List() { 4, 3 }); + + } + } +} diff --git a/src/Konmaripo.Web.Tests.Unit/Services/CachedGitHubServiceTests.cs b/src/Konmaripo.Web.Tests.Unit/Services/CachedGitHubServiceTests.cs index fca1ca5..e3ba406 100644 --- a/src/Konmaripo.Web.Tests.Unit/Services/CachedGitHubServiceTests.cs +++ b/src/Konmaripo.Web.Tests.Unit/Services/CachedGitHubServiceTests.cs @@ -124,7 +124,7 @@ public GitHubRepoBuilder WithId(long id) public GitHubRepo Build() { - return new GitHubRepo(_id,string.Empty,0,false,0,0,DateTimeOffset.Now,DateTimeOffset.Now, string.Empty,false,DateTimeOffset.Now, string.Empty,0); + return new GitHubRepo(_id,string.Empty,0,false,0,0,DateTimeOffset.Now,DateTimeOffset.Now, string.Empty,false,DateTimeOffset.Now, string.Empty,0, new List()); } } diff --git a/src/Konmaripo.Web/Controllers/OrgWideVisibilityController.cs b/src/Konmaripo.Web/Controllers/OrgWideVisibilityController.cs index cbcb38f..b30de09 100644 --- a/src/Konmaripo.Web/Controllers/OrgWideVisibilityController.cs +++ b/src/Konmaripo.Web/Controllers/OrgWideVisibilityController.cs @@ -4,15 +4,34 @@ using System.Threading.Tasks; using Konmaripo.Web.Models; using Konmaripo.Web.Services; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; namespace Konmaripo.Web.Controllers { + public class GitHubRepoEqualityComparer : IEqualityComparer + { + public bool Equals(GitHubRepo x, GitHubRepo y) + { + if (ReferenceEquals(x, y)) return true; + if (ReferenceEquals(x, null)) return false; + if (ReferenceEquals(y, null)) return false; + if (x.GetType() != y.GetType()) return false; + return x.Id == y.Id; + } + + public int GetHashCode(GitHubRepo obj) + { + return obj.Id.GetHashCode(); + } + } + + [Authorize] public class OrgWideVisibilityController : Controller { - private OrgWideVisibilitySettings _settings; - private IGitHubService _gitHubService; + private readonly OrgWideVisibilitySettings _settings; + private readonly IGitHubService _gitHubService; public OrgWideVisibilityController(IOptions visibilitySettings, IGitHubService gitHubService) { @@ -52,8 +71,54 @@ public async Task CreateOrgWideTeam() return RedirectToAction("Index"); } + public async Task RepositoryReconciliation() + { + var comparer = new GitHubRepoEqualityComparer(); + + var allRepos = await _gitHubService.GetRepositoriesForOrganizationAsync(); + var reposWithExemptionTopic = await _gitHubService.GetRepositoriesWithTopic(_settings.ExemptionTagName); + var reposThatAlreadyHaveTeamAccess = await _gitHubService.GetRepositoriesForTeam(_settings.AllOrgMembersGroupName); + + + var reposToAddTeamTo = + allRepos + .Except(reposThatAlreadyHaveTeamAccess, comparer) + .Except(reposWithExemptionTopic, comparer).ToList(); + + var reposToRemoveTeamFrom = + reposThatAlreadyHaveTeamAccess + .Intersect(reposWithExemptionTopic, comparer) + .ToList(); + + var vm = new RepositoryReconciliationViewModel(_settings.ExemptionTagName, _settings.AllOrgMembersGroupName, reposToAddTeamTo, reposToRemoveTeamFrom); + return View(vm); + } + + [HttpPost] + public async Task RepositoryReconciliation (RepositoryReconciliationViewModel vm) + { + await _gitHubService.AddAllOrgTeamToRepos(vm.RepositoriesToAddAccessTo, vm.AllOrgMemberTeamName); + await _gitHubService.RemoveAllOrgTeamFromRepos(vm.RepositoriesToRemoveAccessFrom, vm.AllOrgMemberTeamName); + + return View("RepositoryReconciliationSuccess"); + } } + public class RepositoryReconciliationViewModel + { + public string ExemptionTagName { get; } + public string AllOrgMemberTeamName { get; } + public List RepositoriesToAddAccessTo { get; } + public List RepositoriesToRemoveAccessFrom { get; } + + public RepositoryReconciliationViewModel(string exemptionTagName, string allOrgMemberTeamName, List reposToAdd, List reposToRemove) + { + ExemptionTagName = exemptionTagName; + AllOrgMemberTeamName = allOrgMemberTeamName; + RepositoriesToAddAccessTo = reposToAdd; + RepositoriesToRemoveAccessFrom = reposToRemove; + } + } public class OrgWideVisibilityIndexVM { public string OrgWideTeamName { get; } diff --git a/src/Konmaripo.Web/Models/GitHubRepo.cs b/src/Konmaripo.Web/Models/GitHubRepo.cs index 91008a6..4af4a6c 100644 --- a/src/Konmaripo.Web/Models/GitHubRepo.cs +++ b/src/Konmaripo.Web/Models/GitHubRepo.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Functional.Maybe; namespace Konmaripo.Web.Models @@ -18,8 +19,9 @@ public class GitHubRepo public Maybe PushedDate { get; } public string RepoUrl { get; } public int Subscribers { get; } + public IReadOnlyList Topics { get; } - public GitHubRepo(long repoId, string name, int starCount, bool isArchived, int forkCount, int openIssues, DateTimeOffset createdDate, DateTimeOffset updatedDate, string description, bool isPrivate, DateTimeOffset? pushedDate, string url, int subscribers) + public GitHubRepo(long repoId, string name, int starCount, bool isArchived, int forkCount, int openIssues, DateTimeOffset createdDate, DateTimeOffset updatedDate, string description, bool isPrivate, DateTimeOffset? pushedDate, string url, int subscribers, IReadOnlyList topics) { Name = name; StarCount = starCount; @@ -34,6 +36,7 @@ public GitHubRepo(long repoId, string name, int starCount, bool isArchived, int PushedDate = pushedDate.ToMaybe(); RepoUrl = url; Subscribers = subscribers; + Topics = topics; } } } \ No newline at end of file diff --git a/src/Konmaripo.Web/Models/OrgWideVisibilitySettings.cs b/src/Konmaripo.Web/Models/OrgWideVisibilitySettings.cs index a50f07a..10d2e05 100644 --- a/src/Konmaripo.Web/Models/OrgWideVisibilitySettings.cs +++ b/src/Konmaripo.Web/Models/OrgWideVisibilitySettings.cs @@ -4,5 +4,6 @@ public class OrgWideVisibilitySettings { public string AllOrgMembersGroupName { get; set; } public string AllOrgMembersGroupDescription { get; set; } + public string ExemptionTagName { get; set; } } } \ No newline at end of file diff --git a/src/Konmaripo.Web/Services/CachedGitHubService.cs b/src/Konmaripo.Web/Services/CachedGitHubService.cs index d264e8a..335aa17 100644 --- a/src/Konmaripo.Web/Services/CachedGitHubService.cs +++ b/src/Konmaripo.Web/Services/CachedGitHubService.cs @@ -58,7 +58,7 @@ public async Task ArchiveRepository(long repoId, string repoName) var repos = _memoryCache.Get>(RepoCacheKey); var item = repos.First(x => x.Id == repoId); - var archivedItem = new GitHubRepo(item.Id, item.Name, item.StarCount, true, item.ForkCount, item.OpenIssueCount, item.CreatedDate, item.UpdatedDate, item.Description, item.IsPrivate, item.PushedDate.ToNullable(), item.RepoUrl, item.Subscribers); + var archivedItem = new GitHubRepo(item.Id, item.Name, item.StarCount, true, item.ForkCount, item.OpenIssueCount, item.CreatedDate, item.UpdatedDate, item.Description, item.IsPrivate, item.PushedDate.ToNullable(), item.RepoUrl, item.Subscribers, item.Topics); repos.Remove(item); repos.Add(archivedItem); @@ -138,6 +138,11 @@ public async Task AddMembersToTeam(int teamId, List loginsToAdd) await _gitHubService.AddMembersToTeam(teamId, loginsToAdd); } + public Task> GetRepositoriesWithTopic(string topicName) + { + return _gitHubService.GetRepositoriesWithTopic(topicName); + } + public async Task> GetUsersNotInTeam(string teamName) { var allTeams = await GetAllTeams(); @@ -148,6 +153,21 @@ public async Task> GetUsersNotInTeam(string teamName) return allOrgMembers.Except(teamMembers, new OctokitUserEqualityComparer()).ToList(); } + + public Task> GetRepositoriesForTeam(string teamName) + { + return _gitHubService.GetRepositoriesForTeam(teamName); + } + + public Task AddAllOrgTeamToRepos(List vmRepositoriesToAddAccessTo, string teamName) + { + return _gitHubService.AddAllOrgTeamToRepos(vmRepositoriesToAddAccessTo, teamName); + } + + public Task RemoveAllOrgTeamFromRepos(List vmRepositoriesToRemoveAccessFrom, string teamName) + { + return _gitHubService.RemoveAllOrgTeamFromRepos(vmRepositoriesToRemoveAccessFrom, teamName); + } } public class OctokitUserEqualityComparer : IEqualityComparer diff --git a/src/Konmaripo.Web/Services/GitHubService.cs b/src/Konmaripo.Web/Services/GitHubService.cs index 7e6b236..95308e5 100644 --- a/src/Konmaripo.Web/Services/GitHubService.cs +++ b/src/Konmaripo.Web/Services/GitHubService.cs @@ -3,8 +3,10 @@ using System.IO; using System.IO.Compression; using System.Linq; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Konmaripo.Web.Models; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; using Microsoft.Extensions.Options; using Octokit; using Serilog; @@ -13,6 +15,14 @@ namespace Konmaripo.Web.Services { + public static class ExtensionMethods + { + public static GitHubRepo ToKonmaripoRepo(this Repository x) + { + return new GitHubRepo(x.Id, x.Name, x.StargazersCount, x.Archived, x.ForksCount, x.OpenIssuesCount, x.CreatedAt, x.UpdatedAt, x.Description, x.Private, x.PushedAt, x.HtmlUrl, x.SubscribersCount, x.Topics); + } + } + public class GitHubService : IGitHubService { private readonly IGitHubClient _githubClient; @@ -29,13 +39,44 @@ public GitHubService(IGitHubClient githubClient, IOptions github _archiver = archiver ?? throw new ArgumentNullException(nameof(archiver)); } + public async Task> GetRepositoriesForTeam(string teamName) + { + var allTeams = await GetAllTeams(); + var teamId = allTeams.Single(x => x.Name.Equals(teamName, StringComparison.InvariantCultureIgnoreCase)).Id; + + var repos = await _githubClient.Organization.Team.GetAllRepositories(teamId); + return repos.Select(x => x.ToKonmaripoRepo()).ToList(); + } + + public async Task AddAllOrgTeamToRepos(List vmRepositoriesToAddAccessTo, string teamName) + { + var allTeams = await GetAllTeams(); + var teamId = allTeams.Single(x => x.Name.Equals(teamName, StringComparison.InvariantCultureIgnoreCase)).Id; + + foreach (var repo in vmRepositoriesToAddAccessTo) + { + await _githubClient.Organization.Team.AddRepository(teamId, _gitHubSettings.OrganizationName, repo.Name); + } + } + + public async Task RemoveAllOrgTeamFromRepos(List vmRepositoriesToRemoveAccessFrom, string teamName) + { + var allTeams = await GetAllTeams(); + var teamId = allTeams.Single(x => x.Name.Equals(teamName, StringComparison.InvariantCultureIgnoreCase)).Id; + + foreach (var repo in vmRepositoriesToRemoveAccessFrom) + { + await _githubClient.Organization.Team.RemoveRepository(teamId, _gitHubSettings.OrganizationName, repo.Name); + } + } + public async Task> GetRepositoriesForOrganizationAsync() { var orgName = _gitHubSettings.OrganizationName; var repos = await _githubClient.Repository.GetAllForOrg(orgName); - return repos.Select(x => new GitHubRepo(x.Id, x.Name, x.StargazersCount, x.Archived, x.ForksCount, x.OpenIssuesCount, x.CreatedAt, x.UpdatedAt, x.Description, x.Private, x.PushedAt, x.HtmlUrl, x.SubscribersCount)).ToList(); + return repos.Select(x => x.ToKonmaripoRepo()).ToList(); } public async Task GetExtendedRepoInformationFor(long repoId) @@ -193,5 +234,15 @@ public async Task AddMembersToTeam(int teamId, List loginsToAdd) await _githubClient.Organization.Team.AddOrEditMembership(teamId, login, request); } } + + public async Task> GetRepositoriesWithTopic(string topicName) + { + var allRepos = await GetRepositoriesForOrganizationAsync(); + + var reposWithTopic = allRepos.Where(x=>x.Topics.Any(x=>x.Equals(topicName, StringComparison.InvariantCultureIgnoreCase))).ToList(); + + return reposWithTopic; + // TODO Filter repos by topic. + } } } diff --git a/src/Konmaripo.Web/Services/IGitHubService.cs b/src/Konmaripo.Web/Services/IGitHubService.cs index aa70300..eb0f0df 100644 --- a/src/Konmaripo.Web/Services/IGitHubService.cs +++ b/src/Konmaripo.Web/Services/IGitHubService.cs @@ -27,5 +27,9 @@ public interface IGitHubService Task> GetTeamMembers(int teamId); Task AddMembersToTeam(string teamName, List loginsToAdd); Task AddMembersToTeam(int teamId, List loginsToAdd); + Task> GetRepositoriesWithTopic(string topicName); + Task> GetRepositoriesForTeam(string teamName); + Task AddAllOrgTeamToRepos(List vmRepositoriesToAddAccessTo, string teamName); + Task RemoveAllOrgTeamFromRepos(List vmRepositoriesToRemoveAccessFrom, string teamName); } } \ No newline at end of file diff --git a/src/Konmaripo.Web/Views/OrgWideVisibility/AddOrgMembers.cshtml b/src/Konmaripo.Web/Views/OrgWideVisibility/AddOrgMembers.cshtml index 3d76883..69b3d78 100644 --- a/src/Konmaripo.Web/Views/OrgWideVisibility/AddOrgMembers.cshtml +++ b/src/Konmaripo.Web/Views/OrgWideVisibility/AddOrgMembers.cshtml @@ -11,22 +11,34 @@

Step 2: Add Missing team members

-

The @Model.Count below organization members are not a part of the org-wide group.

-
-@foreach (var row in Model.ToArray().Split(4)) +@if (!Model.Any()) { -
- @foreach (var login in row) +
+

Great! No missing members.

+

All your org's members are within the group.

+ @Html.ActionLink($"Next step: Grant/Remove Team Access to Repositories", "RepositoryReconciliation", "OrgWideVisibility", null, new { @class = "btn btn-success", role = "button" }) +
+ +} +else +{ +
+ @foreach (var row in Model.ToArray().Split(4)) { -
- @login +
+ @foreach (var login in row) + { +
+ @login +
+ }
}
+
+ @Html.ActionLink($"Add these {Model.Count} members to the group.", "AddOrgMembersList", "OrgWideVisibility", new { loginsToAdd = Model }, new { @class = "btn btn-success", role = "button" }) +
} -
-
- @Html.ActionLink($"Add these {Model.Count} members to the group.", "AddOrgMembersList", "OrgWideVisibility", new { loginsToAdd = Model }, new { @class = "btn btn-success", role = "button" }) -
+ diff --git a/src/Konmaripo.Web/Views/OrgWideVisibility/RepositoryReconciliation.cshtml b/src/Konmaripo.Web/Views/OrgWideVisibility/RepositoryReconciliation.cshtml new file mode 100644 index 0000000..7bb4c63 --- /dev/null +++ b/src/Konmaripo.Web/Views/OrgWideVisibility/RepositoryReconciliation.cshtml @@ -0,0 +1,66 @@ +@using Konmaripo.Web.Controllers +@model RepositoryReconciliationViewModel +@{ + ViewData["Title"] = "Org-Wide Visibility"; +} + +
+

Org-Wide Visibility

+

Ensuring all organization members can see all private repositories.

+
+ +
+

Step 3: Repository Reconciliation

+
+ +@if (!(Model.RepositoriesToAddAccessTo.Any() || Model.RepositoriesToRemoveAccessFrom.Any())) +{ +
+

No team access changes needed

+

The "@Model.AllOrgMemberTeamName" team has access to all repos except repos tagged with "@Model.ExemptionTagName".

+ @Html.ActionLink($"Return to Home", "Index", "Home", null, new { @class = "btn btn-success", role = "button" }) +
+} +else +{ + if (Model.RepositoriesToRemoveAccessFrom.Any()) + { +
+

The following repositories will have access removed for the "@Model.AllOrgMemberTeamName" team:

+
+
+
    + @foreach (var repo in Model.RepositoriesToRemoveAccessFrom) + { +
  • @repo.Name
  • + } +
+
+
+
+ } + if (Model.RepositoriesToAddAccessTo.Any()) + { +
+

The following repositories will have access added for the "@Model.AllOrgMemberTeamName" team. To prevent this, add the "@Model.ExemptionTagName" topic in the repository's settings.

+
+
+
    + @foreach (var repo in Model.RepositoriesToAddAccessTo) + { +
  • @repo.Name
  • + } +
+
+
+
+ } + + +} \ No newline at end of file diff --git a/src/Konmaripo.Web/Views/OrgWideVisibility/RepositoryReconciliationSuccess.cshtml b/src/Konmaripo.Web/Views/OrgWideVisibility/RepositoryReconciliationSuccess.cshtml new file mode 100644 index 0000000..c63475f --- /dev/null +++ b/src/Konmaripo.Web/Views/OrgWideVisibility/RepositoryReconciliationSuccess.cshtml @@ -0,0 +1,18 @@ +@using Konmaripo.Web.Controllers +@{ + ViewData["Title"] = "Org-Wide Visibility"; +} + +
+

Org-Wide Visibility

+

Ensuring all organization members can see all private repositories.

+
+ +
+

Step 3: Repository Reconciliation

+
+ +
+

Repository Reconciliation succeeded.

+ @Html.ActionLink($"Return to Home", "Index", "Home", null, new { @class = "btn btn-success", role = "button" }) +
diff --git a/src/Konmaripo.Web/appsettings.json b/src/Konmaripo.Web/appsettings.json index d3eaab7..fdf52d1 100644 --- a/src/Konmaripo.Web/appsettings.json +++ b/src/Konmaripo.Web/appsettings.json @@ -7,7 +7,7 @@ "OrgWideVisibilitySettings": { "AllOrgMembersGroupName": "all-org-members", "AllOrgMembersGroupDescription": "A group created by the Konmaripo tool, which includes everyone who is a member of the GitHub organization.", - "ExemptionTagName": "ExemptFromOrgWideVisibility" + "ExemptionTagName": "exempt-from-org-visibility" }, "ArchivalSettings": { "ArchivalUrl": "CHANGE_ME"