-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #174 from NoxOrg/Feature/NOX-764-Project-security
Feature/nox 764 project security
- Loading branch information
Showing
26 changed files
with
557 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
153 changes: 153 additions & 0 deletions
153
src/Nox.Cli.Plugins/Nox.Cli.Plugin.AzDevOps/AzDevopsAddProjectAadGroup_v1.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
using Nox.Cli.Abstractions; | ||
using Nox.Cli.Abstractions.Extensions; | ||
using Nox.Cli.Plugin.AzDevOps.Clients; | ||
using Nox.Cli.Plugin.AzDevOps.Enums; | ||
|
||
namespace Nox.Cli.Plugin.AzDevOps; | ||
|
||
public class AzDevopsAddProjectAadGroup_v1 : INoxCliAddin | ||
{ | ||
public NoxActionMetaData Discover() | ||
{ | ||
return new NoxActionMetaData | ||
{ | ||
Name = "azdevops/add-project-aad-group@v1", | ||
Author = "Jan Schutte", | ||
Description = "Add an AAD group to a DevOps project group", | ||
|
||
Inputs = | ||
{ | ||
["server"] = new NoxActionInput { | ||
Id = "server", | ||
Description = "The DevOps server hostname or IP", | ||
Default = "localhost", | ||
IsRequired = true | ||
}, | ||
|
||
["personal-access-token"] = new NoxActionInput { | ||
Id = "personal-access-token", | ||
Description = "The personal access token to connect to DevOps with", | ||
Default = string.Empty, | ||
IsRequired = true | ||
}, | ||
["project-id"] = new NoxActionInput | ||
{ | ||
Id = "project-id", | ||
Description = "The DevOps project Id", | ||
Default = Guid.Empty, | ||
IsRequired = true | ||
}, | ||
["project-group-name"] = new NoxActionInput | ||
{ | ||
Id = "project-group-name", | ||
Description = "The DevOps project group to add the AAD group to", | ||
Default = string.Empty, | ||
IsRequired = true | ||
}, | ||
["aad-group-name"] = new NoxActionInput | ||
{ | ||
Id = "aad-group-name", | ||
Description = "The AAD group to add", | ||
Default = Guid.Empty, | ||
IsRequired = true | ||
} | ||
} | ||
}; | ||
} | ||
|
||
private string? _server; | ||
private string? _pat; | ||
private Guid? _projectId; | ||
private string? _projectGroupName; | ||
private string? _aadGroupName; | ||
private bool _isServerContext = false; | ||
|
||
public Task BeginAsync(IDictionary<string, object> inputs) | ||
{ | ||
_server = inputs.Value<string>("server"); | ||
_pat = inputs.Value<string>("personal-access-token"); | ||
_projectId = inputs.Value<Guid>("project-id"); | ||
_aadGroupName = inputs.Value<string>("project-group-name"); | ||
_aadGroupName = inputs.Value<string>("aad-group-name"); | ||
return Task.CompletedTask; | ||
} | ||
|
||
public async Task<IDictionary<string, object>> ProcessAsync(INoxWorkflowContext ctx) | ||
{ | ||
_isServerContext = ctx.IsServer; | ||
var outputs = new Dictionary<string, object>(); | ||
|
||
ctx.SetState(ActionState.Error); | ||
|
||
if (string.IsNullOrWhiteSpace(_server) || | ||
string.IsNullOrWhiteSpace(_pat) || | ||
_projectId == null || | ||
_projectId == Guid.Empty || | ||
string.IsNullOrEmpty(_projectGroupName) || | ||
string.IsNullOrEmpty(_aadGroupName)) | ||
{ | ||
ctx.SetErrorMessage("The devops add-project-aad-group action was not initialized"); | ||
} | ||
else | ||
{ | ||
try | ||
{ | ||
var result = await AddAdmins(ctx); | ||
if (result) | ||
{ | ||
ctx.SetState(ActionState.Success); | ||
} | ||
} | ||
catch (Exception ex) | ||
{ | ||
ctx.SetErrorMessage(ex.Message); | ||
} | ||
} | ||
|
||
return outputs; | ||
} | ||
|
||
public Task EndAsync() | ||
{ | ||
return Task.CompletedTask; | ||
} | ||
|
||
private async Task<bool> AddAdmins(INoxWorkflowContext ctx) | ||
{ | ||
var identityPickerClient = new IdentityPickerClient(_server!, _pat!); | ||
var graphClient = new GraphClient(_server!, _pat!); | ||
|
||
var aadGroups = await identityPickerClient.FindIdentity(_aadGroupName!, IdentityType.Group); | ||
if (aadGroups == null) | ||
{ | ||
ctx.SetErrorMessage($"Unable to locate the AAD group: {_aadGroupName}"); | ||
return false; | ||
} | ||
|
||
var aadGroup = aadGroups.First(); | ||
|
||
|
||
var projectDescriptor = await graphClient.GetDescriptor(_projectId.ToString()!); | ||
if (!_projectGroupName!.StartsWith('\\')) _projectGroupName = '\\' + _projectGroupName; | ||
var projectGroup = await graphClient.FindProjectGroup(projectDescriptor!, _projectGroupName); | ||
if (projectGroup == null) | ||
{ | ||
ctx.SetErrorMessage($"Unable to locate the project administrator group for this DevOps project"); | ||
return false; | ||
} | ||
|
||
var aadGroupDescriptor = await graphClient.GetDescriptor(projectGroup.Descriptor!, aadGroup.OriginId!); | ||
|
||
if (string.IsNullOrEmpty(aadGroupDescriptor)) | ||
{ | ||
ctx.SetErrorMessage($"Unable to retrieve AAD group descriptor."); | ||
return false; | ||
} | ||
|
||
return await graphClient.AddGroupMembership(projectGroup.Descriptor!, aadGroupDescriptor!); | ||
|
||
|
||
} | ||
|
||
|
||
} |
118 changes: 118 additions & 0 deletions
118
src/Nox.Cli.Plugins/Nox.Cli.Plugin.AzDevOps/Clients/GraphClient.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
using System.Text.Json; | ||
using Microsoft.VisualStudio.Services.Graph.Client; | ||
using Nox.Cli.Abstractions.Helpers; | ||
using Nox.Cli.Plugin.AzDevOps.DTO; | ||
using Nox.Cli.Plugin.AzDevOps.Exceptions; | ||
using Nox.Cli.Plugin.AzDevOps.Helpers; | ||
using RestSharp; | ||
|
||
namespace Nox.Cli.Plugin.AzDevOps.Clients; | ||
|
||
public class GraphClient | ||
{ | ||
private readonly RestClient _client; | ||
private readonly string _pat; | ||
|
||
|
||
public GraphClient(string serverUri, string pat) | ||
{ | ||
var builder = new UriBuilder(new Uri(serverUri)); | ||
builder.Host = $"vssps.{builder.Host}"; | ||
serverUri = builder.Uri.ToString(); | ||
|
||
_client = new RestClient(serverUri); | ||
_pat = pat; | ||
} | ||
|
||
public async Task<string?> GetDescriptor(string storageKey) | ||
{ | ||
var request = new RestRequest($"/_apis/graph/descriptors/{storageKey}") | ||
{ | ||
Method = Method.Get | ||
}; | ||
|
||
AddHeaders(request); | ||
var response = await _client.ExecuteAsync<DescriptorResponse>(request); | ||
if (!response.IsSuccessStatusCode) | ||
{ | ||
throw new DevOpsClientException($"Unable to find Descriptor for storage key: {storageKey}"); | ||
} | ||
|
||
return response.Data!.Value!; | ||
} | ||
|
||
public async Task<string?> GetDescriptor(string projectGroupDescriptor, string originId) | ||
{ | ||
var request = new RestRequest($"/_apis/graph/groups") | ||
{ | ||
Method = Method.Post | ||
}; | ||
request.AddQueryParameter("groupDescriptors", projectGroupDescriptor); | ||
var payload = new AadDescriptorRequest | ||
{ | ||
OriginId = originId | ||
}; | ||
request.AddJsonBody(JsonSerializer.Serialize(payload, JsonOptions.Instance)); | ||
AddHeaders(request); | ||
var response = await _client.ExecuteAsync<AadDescriptorResponse>(request); | ||
if (!response.IsSuccessStatusCode) | ||
{ | ||
throw new DevOpsClientException($"Unable to find Descriptor for OriginId: {originId}, StorageKey:"); | ||
} | ||
|
||
return response.Data!.Descriptor!; | ||
} | ||
|
||
public async Task<GraphGroupResult?> FindProjectGroup(string projectDescriptor, string query) | ||
{ | ||
var request = new RestRequest($"/_apis/graph/groups") | ||
{ | ||
Method = Method.Get | ||
}; | ||
request.AddQueryParameter("scopeDescriptor", projectDescriptor); | ||
AddHeaders(request); | ||
var response = await _client.ExecuteAsync<GraphGroupPagedResponse>(request); | ||
if (!response.IsSuccessStatusCode) | ||
{ | ||
throw new DevOpsClientException($"Unable to find group: {query} ({response.ErrorMessage})"); | ||
} | ||
|
||
foreach (var group in response.Data!.Value!) | ||
{ | ||
if (group.PrincipalName!.Contains(query, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
return group; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
public async Task<bool> AddGroupMembership(string projectGroupDescriptor, string memberDescriptor) | ||
{ | ||
var request = new RestRequest($"/_apis/Graph/Memberships/{memberDescriptor}/{projectGroupDescriptor}") | ||
{ | ||
Method = Method.Put | ||
}; | ||
AddHeaders(request); | ||
var response = await _client.ExecuteAsync<MembershipResponse>(request); | ||
if (!response.IsSuccessStatusCode) | ||
{ | ||
throw new DevOpsClientException($"An error occurred while trying to add project group membership ({response.ErrorMessage})"); | ||
} | ||
|
||
if (response.Data!.ContainerDescriptor == projectGroupDescriptor && response.Data.MemberDescriptor == memberDescriptor) | ||
{ | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
private void AddHeaders(RestRequest request) | ||
{ | ||
request.AddHeader("Authorization", $"Basic {_pat.ToEncoded()}"); | ||
request.AddHeader("Content-Type", "application/json"); | ||
request.AddHeader("Accept", "application/json;api-version=7.1-preview.1"); | ||
} | ||
} |
Oops, something went wrong.