Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Process for Reconcilling access for "all org" group to repositories #194

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/Konmaripo.Web.Tests.Unit/QuickLinqTest.cs
Original file line number Diff line number Diff line change
@@ -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<int> { 1, 2, 3, 4, 5 };
var firstExcept = new List<int> { 1, 2};
var secondExcept = new List<int> { 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<int>() { 4, 3 });

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>());
}

}
Expand Down
69 changes: 67 additions & 2 deletions src/Konmaripo.Web/Controllers/OrgWideVisibilityController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<GitHubRepo>
{
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<OrgWideVisibilitySettings> visibilitySettings, IGitHubService gitHubService)
{
Expand Down Expand Up @@ -52,8 +71,54 @@ public async Task<IActionResult> CreateOrgWideTeam()

return RedirectToAction("Index");
}
public async Task<IActionResult> 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<IActionResult> 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<GitHubRepo> RepositoriesToAddAccessTo { get; }
public List<GitHubRepo> RepositoriesToRemoveAccessFrom { get; }

public RepositoryReconciliationViewModel(string exemptionTagName, string allOrgMemberTeamName, List<GitHubRepo> reposToAdd, List<GitHubRepo> reposToRemove)
{
ExemptionTagName = exemptionTagName;
AllOrgMemberTeamName = allOrgMemberTeamName;
RepositoriesToAddAccessTo = reposToAdd;
RepositoriesToRemoveAccessFrom = reposToRemove;
}
}
public class OrgWideVisibilityIndexVM
{
public string OrgWideTeamName { get; }
Expand Down
5 changes: 4 additions & 1 deletion src/Konmaripo.Web/Models/GitHubRepo.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Functional.Maybe;

namespace Konmaripo.Web.Models
Expand All @@ -18,8 +19,9 @@ public class GitHubRepo
public Maybe<DateTimeOffset> PushedDate { get; }
public string RepoUrl { get; }
public int Subscribers { get; }
public IReadOnlyList<string> 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<string> topics)
{
Name = name;
StarCount = starCount;
Expand All @@ -34,6 +36,7 @@ public GitHubRepo(long repoId, string name, int starCount, bool isArchived, int
PushedDate = pushedDate.ToMaybe();
RepoUrl = url;
Subscribers = subscribers;
Topics = topics;
}
}
}
1 change: 1 addition & 0 deletions src/Konmaripo.Web/Models/OrgWideVisibilitySettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ public class OrgWideVisibilitySettings
{
public string AllOrgMembersGroupName { get; set; }
public string AllOrgMembersGroupDescription { get; set; }
public string ExemptionTagName { get; set; }
}
}
22 changes: 21 additions & 1 deletion src/Konmaripo.Web/Services/CachedGitHubService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public async Task ArchiveRepository(long repoId, string repoName)
var repos = _memoryCache.Get<List<GitHubRepo>>(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);

Expand Down Expand Up @@ -138,6 +138,11 @@ public async Task AddMembersToTeam(int teamId, List<string> loginsToAdd)
await _gitHubService.AddMembersToTeam(teamId, loginsToAdd);
}

public Task<List<GitHubRepo>> GetRepositoriesWithTopic(string topicName)
{
return _gitHubService.GetRepositoriesWithTopic(topicName);
}

public async Task<List<User>> GetUsersNotInTeam(string teamName)
{
var allTeams = await GetAllTeams();
Expand All @@ -148,6 +153,21 @@ public async Task<List<User>> GetUsersNotInTeam(string teamName)

return allOrgMembers.Except(teamMembers, new OctokitUserEqualityComparer()).ToList();
}

public Task<List<GitHubRepo>> GetRepositoriesForTeam(string teamName)
{
return _gitHubService.GetRepositoriesForTeam(teamName);
}

public Task AddAllOrgTeamToRepos(List<GitHubRepo> vmRepositoriesToAddAccessTo, string teamName)
{
return _gitHubService.AddAllOrgTeamToRepos(vmRepositoriesToAddAccessTo, teamName);
}

public Task RemoveAllOrgTeamFromRepos(List<GitHubRepo> vmRepositoriesToRemoveAccessFrom, string teamName)
{
return _gitHubService.RemoveAllOrgTeamFromRepos(vmRepositoriesToRemoveAccessFrom, teamName);
}
}

public class OctokitUserEqualityComparer : IEqualityComparer<User>
Expand Down
53 changes: 52 additions & 1 deletion src/Konmaripo.Web/Services/GitHubService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -29,13 +39,44 @@ public GitHubService(IGitHubClient githubClient, IOptions<GitHubSettings> github
_archiver = archiver ?? throw new ArgumentNullException(nameof(archiver));
}

public async Task<List<GitHubRepo>> 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<GitHubRepo> 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<GitHubRepo> 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<List<GitHubRepo>> 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<ExtendedRepoInformation> GetExtendedRepoInformationFor(long repoId)
Expand Down Expand Up @@ -193,5 +234,15 @@ public async Task AddMembersToTeam(int teamId, List<string> loginsToAdd)
await _githubClient.Organization.Team.AddOrEditMembership(teamId, login, request);
}
}

public async Task<List<GitHubRepo>> 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.
}
}
}
4 changes: 4 additions & 0 deletions src/Konmaripo.Web/Services/IGitHubService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,9 @@ public interface IGitHubService
Task<IReadOnlyList<User>> GetTeamMembers(int teamId);
Task AddMembersToTeam(string teamName, List<string> loginsToAdd);
Task AddMembersToTeam(int teamId, List<string> loginsToAdd);
Task<List<GitHubRepo>> GetRepositoriesWithTopic(string topicName);
Task<List<GitHubRepo>> GetRepositoriesForTeam(string teamName);
Task AddAllOrgTeamToRepos(List<GitHubRepo> vmRepositoriesToAddAccessTo, string teamName);
Task RemoveAllOrgTeamFromRepos(List<GitHubRepo> vmRepositoriesToRemoveAccessFrom, string teamName);
}
}
34 changes: 23 additions & 11 deletions src/Konmaripo.Web/Views/OrgWideVisibility/AddOrgMembers.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,34 @@

<div class="text-center">
<h2>Step 2: Add Missing team members</h2>
<p>The @Model.Count below organization members are not a part of the org-wide group.</p>
</div>

<div class="container">
@foreach (var row in Model.ToArray().Split(4))
@if (!Model.Any())
{
<div class="row">
@foreach (var login in row)
<div class="alert alert-success">
<h4 class="alert-heading">Great! No missing members.</h4>
<p>All your org's members are within the group.</p>
@Html.ActionLink($"Next step: Grant/Remove Team Access to Repositories", "RepositoryReconciliation", "OrgWideVisibility", null, new { @class = "btn btn-success", role = "button" })
</div>

}
else
{
<div class="container">
@foreach (var row in Model.ToArray().Split(4))
{
<div class="col">
@login
<div class="row">
@foreach (var login in row)
{
<div class="col">
@login
</div>
}
</div>
}
</div>
<div>
@Html.ActionLink($"Add these {Model.Count} members to the group.", "AddOrgMembersList", "OrgWideVisibility", new { loginsToAdd = Model }, new { @class = "btn btn-success", role = "button" })
</div>
}
</div>
<div>
@Html.ActionLink($"Add these {Model.Count} members to the group.", "AddOrgMembersList", "OrgWideVisibility", new { loginsToAdd = Model }, new { @class = "btn btn-success", role = "button" })
</div>

Loading