Skip to content

Commit

Permalink
Merge pull request #259 from NoxOrg/feature/implement-find-aad-group-…
Browse files Browse the repository at this point in the history
…changes

- added Verify Group feature to AzDevOpsVerifyAadGroups_v1.cs plugin
  • Loading branch information
jan-schutte authored Jul 10, 2024
2 parents 3afe14e + e425b1b commit ac614ce
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 22 deletions.
9 changes: 9 additions & 0 deletions src/Nox.Cli.Abstractions/Secrets/IPersistedSecretStoreEx.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Nox.Secrets.Abstractions;
namespace Nox.Cli.Abstractions.Secrets;
public interface IPersistedSecretStoreEx: IPersistedSecretStore
{
#if NET8_0
Task SaveAsync(string key, string secret);
Task<string?> LoadAsync(string key, TimeSpan? validFor = null);
#endif
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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 AzDevOpsVerifyAadGroup_v1 : INoxCliAddin
{
public NoxActionMetaData Discover()
{
return new NoxActionMetaData
{
Name = "azdevops/verify-aad-group@v1",
Author = "Jan Schutte",
Description = "Verify that an AAD group ia available 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
},
["aad-group-name"] = new NoxActionInput
{
Id = "aad-group-name",
Description = "The AAD group to verify",
Default = Guid.Empty,
IsRequired = true
}
},
Outputs =
{
["is-found"] = new NoxActionOutput {
Id = "is-found",
Description = "A boolean indicating if the AAD group was found.",
}
}
};
}
private string? _server;
private string? _pat;
private Guid? _projectId;
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>("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(_aadGroupName))
{
ctx.SetErrorMessage("The devops verify-aad-group action was not initialized");
}
else
{
try
{
var result = await FindGroup(ctx);
outputs["is-found"] = result;
ctx.SetState(ActionState.Success);
}
catch (Exception ex)
{
ctx.SetErrorMessage(ex.Message);
}
}
return outputs;
}
public Task EndAsync()
{
return Task.CompletedTask;
}
private async Task<bool> FindGroup(INoxWorkflowContext ctx)
{
var identityPickerClient = new IdentityPickerClient(_server!, _pat!);
var aadGroups = await identityPickerClient.FindIdentity(_aadGroupName!, IdentityType.Group);
if (aadGroups == null || aadGroups.Count == 0)
{
return false;
}
return true;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ private async Task<bool> AddAdmins(INoxWorkflowContext ctx)
var graphClient = new GraphClient(_server!, _pat!);

var aadGroups = await identityPickerClient.FindIdentity(_aadGroupName!, IdentityType.Group);
if (aadGroups == null)
if (aadGroups == null || aadGroups.Count == 0)
{
ctx.SetErrorMessage($"Unable to locate the AAD group: {_aadGroupName}");
return false;
Expand Down
13 changes: 0 additions & 13 deletions src/Nox.Cli.Variables/Secrets/IPersistedSecretStore.cs

This file was deleted.

6 changes: 4 additions & 2 deletions src/Nox.Cli.Variables/Secrets/OrgSecretResolver.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
using Nox.Cli.Abstractions.Configuration;
using Nox.Cli.Abstractions.Secrets;
using Nox.Secrets.Abstractions;

namespace Nox.Cli.Variables.Secrets;

public class OrgSecretResolver: IOrgSecretResolver
{
private readonly IPersistedSecretStore _store;
private readonly IPersistedSecretStoreEx _store;

public OrgSecretResolver(IPersistedSecretStore store)
public OrgSecretResolver(IPersistedSecretStoreEx store)
{
_store = store;
}
Expand Down
3 changes: 2 additions & 1 deletion src/Nox.Cli.Variables/Secrets/PersistedSecretStore.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using Microsoft.AspNetCore.DataProtection;
using Nox.Cli.Abstractions.Secrets;
using Nox.Solution.Constants;
using Nox.Types;
using DateTime = System.DateTime;
using File = System.IO.File;

namespace Nox.Cli.Variables.Secrets;

public class PersistedSecretStore: IPersistedSecretStore
public class PersistedSecretStore: IPersistedSecretStoreEx
{
private readonly IDataProtector _protector;
private const string ProtectorPurpose = "nox-secrets";
Expand Down
5 changes: 3 additions & 2 deletions src/Nox.Cli.Variables/Secrets/ServerSecretResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Nox.Cli.Abstractions.Configuration;
using Nox.Cli.Abstractions.Secrets;
using Nox.Cli.Server.Abstractions;

namespace Nox.Cli.Variables.Secrets;
Expand All @@ -10,12 +11,12 @@ public class ServerSecretResolver: IServerSecretResolver
{
private static readonly Regex SecretsVariableRegex = new(@"\$\{\{\s*server\.secrets\.(?<variable>[\w\.\-_:]+)\s*\}\}", RegexOptions.Compiled | RegexOptions.IgnoreCase);

private readonly IPersistedSecretStore _store;
private readonly IPersistedSecretStoreEx _store;
private readonly string _tenantId;
private readonly string _clientId;
private readonly string _clientSecret;

public ServerSecretResolver(IPersistedSecretStore store, string tenantId, string clientId, string clientSecret)
public ServerSecretResolver(IPersistedSecretStoreEx store, string tenantId, string clientId, string clientSecret)
{
_store = store;
_tenantId = tenantId;
Expand Down
7 changes: 5 additions & 2 deletions src/Nox.Cli.Variables/Secrets/ServiceExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Nox.Cli.Abstractions.Secrets;
using Nox.Secrets.Abstractions;

namespace Nox.Cli.Variables.Secrets;

Expand All @@ -12,14 +14,15 @@ public static IServiceCollection AddOrgSecretResolver(this IServiceCollection se

public static IServiceCollection AddServerSecretResolver(this IServiceCollection services, string tenantId, string clientId, string clientSecret)
{
services.AddSingleton<IServerSecretResolver>(sp => new ServerSecretResolver(sp.GetRequiredService<IPersistedSecretStore>(), tenantId, clientId, clientSecret));
services.AddSingleton<IServerSecretResolver>(sp => new ServerSecretResolver(sp.GetRequiredService<IPersistedSecretStoreEx>(), tenantId, clientId, clientSecret));
return services;
}

public static IServiceCollection AddPersistedSecretStore(this IServiceCollection services)
{
services.AddDataProtection();
services.AddSingleton<IPersistedSecretStore, PersistedSecretStore>();
services.AddSingleton<IPersistedSecretStore, Nox.Secrets.PersistedSecretStore>();
services.AddSingleton<IPersistedSecretStoreEx, PersistedSecretStore>();
return services;
}
}
7 changes: 7 additions & 0 deletions src/Nox.Cli.sln
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugin.Git.Tests", "..\test
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestHelpers", "TestHelpers\TestHelpers.csproj", "{413D3BDF-2572-4DD0-A2F7-5FBEFD9DAC19}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugin.AzDevOps.Tests", "..\tests\Plugin.AzDevOps.Tests\Plugin.AzDevOps.Tests.csproj", "{8617ACC1-A6C8-4E26-BD79-E9A9D854D0BE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -221,6 +223,10 @@ Global
{413D3BDF-2572-4DD0-A2F7-5FBEFD9DAC19}.Debug|Any CPU.Build.0 = Debug|Any CPU
{413D3BDF-2572-4DD0-A2F7-5FBEFD9DAC19}.Release|Any CPU.ActiveCfg = Release|Any CPU
{413D3BDF-2572-4DD0-A2F7-5FBEFD9DAC19}.Release|Any CPU.Build.0 = Release|Any CPU
{8617ACC1-A6C8-4E26-BD79-E9A9D854D0BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8617ACC1-A6C8-4E26-BD79-E9A9D854D0BE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8617ACC1-A6C8-4E26-BD79-E9A9D854D0BE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8617ACC1-A6C8-4E26-BD79-E9A9D854D0BE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -260,6 +266,7 @@ Global
{45F0637F-945A-42BD-8A2D-5FCFFDA741C8} = {E3FA4748-9B96-42E7-BDE8-1F08AA8AD4D9}
{8309A464-7247-4939-AE15-C59B5EC3D516} = {BDFC35C7-EFDC-4502-B69B-45ECB3C805CE}
{413D3BDF-2572-4DD0-A2F7-5FBEFD9DAC19} = {70B359E7-710C-4583-99A0-1F59333C3AD1}
{8617ACC1-A6C8-4E26-BD79-E9A9D854D0BE} = {BDFC35C7-EFDC-4502-B69B-45ECB3C805CE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B440C24E-F11D-4F28-9EE5-F7AB214679E6}
Expand Down
34 changes: 34 additions & 0 deletions tests/Plugin.AzDevOps.Tests/DevOpsIntegrationFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Nox;
using Nox.Cli.Abstractions.Caching;
using Nox.Cli.Caching;
using Nox.Cli.Configuration;
using Nox.Cli.PersonalAccessToken;
using Nox.Cli.Variables.Secrets;
using Nox.Secrets.Abstractions;
using Nox.Solution;
namespace Plugin.AzDevOps.Tests;

public class DevOpsIntegrationFixture
{
public IServiceProvider ServiceProvider { get; private set; }
public IServiceCollection? Services { get; private set; }
public DevOpsIntegrationFixture()
{
Services = new ServiceCollection();
Services.AddSingleton(Mock.Of<NoxSolution>());
Services.AddOrgSecretResolver();
Services.AddSingleton(Mock.Of<LocalTaskExecutorConfiguration>());
Services.AddPersistedSecretStore();
Services.AddSingleton<INoxSecretsResolver, NoxSecretsResolver>();
Services.AddSingleton<AzDevOpsPatProvider>();
Services.AddNoxTokenCache();
var onlineCacheUrl = "https://noxorg.dev";
var persistedTokenCache = Services.BuildServiceProvider().GetRequiredService<IPersistedTokenCache>();
var cacheBuilder = new NoxCliCacheBuilder(onlineCacheUrl, false, persistedTokenCache);
var cacheManager = cacheBuilder.Build();
Services.AddNoxCliCacheManager(cacheManager);
ServiceProvider = Services.BuildServiceProvider();
}
}
50 changes: 50 additions & 0 deletions tests/Plugin.AzDevOps.Tests/GroupTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Nox.Cli.Abstractions.Caching;
using Nox.Cli.Actions;
using Nox.Cli.Caching;
using Nox.Cli.Configuration;
using Nox.Cli.PersonalAccessToken;
using Nox.Cli.Plugin.AzDevOps;
using Nox.Cli.Variables.Secrets;
using Nox.Secrets.Abstractions;
using Nox.Solution;
namespace Plugin.AzDevOps.Tests;
public class GroupTests: IClassFixture<DevOpsIntegrationFixture>
{
private readonly DevOpsIntegrationFixture _fixture;
public GroupTests(DevOpsIntegrationFixture fixture)
{
_fixture = fixture;
}

[Theory]
[InlineData("NOX_PROJECTS_ALL", true)]
[InlineData("NOX_PROJECT_DOESNOTEXIST", false)]
public async Task Can_verify_whether_an_aad_group_exists_or_not(string groupName, bool result)
{
var wfConfig = new WorkflowConfiguration();
var sln = _fixture.ServiceProvider.GetRequiredService<NoxSolution>();
var orgResolver = _fixture.ServiceProvider.GetRequiredService<IOrgSecretResolver>();
var cacheManager = _fixture.ServiceProvider.GetRequiredService<INoxCliCacheManager>();
var lteConfig = _fixture.ServiceProvider.GetRequiredService<LocalTaskExecutorConfiguration>();
var secretsResolver = _fixture.ServiceProvider.GetRequiredService<INoxSecretsResolver>();
var tokenCache = _fixture.ServiceProvider.GetRequiredService<IPersistedTokenCache>();
var accessToken = await CredentialHelper.GetAzureDevOpsAccessToken();
var patProvider = new AzDevOpsPatProvider(tokenCache, "iwgplc");
var pat = await patProvider.GetPat(accessToken!);

var plugin = new AzDevOpsVerifyAadGroup_v1();
var inputs = new Dictionary<string, object>
{
{"server", "https://dev.azure.com/iwgplc"},
{"personal-access-token", pat},
{"project-id", "d6aee400-9659-4dec-a309-673518d4cc30"},
{"aad-group-name", groupName}
};
await plugin.BeginAsync(inputs);
var ctx = new NoxWorkflowContext(wfConfig, sln, orgResolver, cacheManager, lteConfig, secretsResolver, null!);
var pluginOutput = await plugin.ProcessAsync(ctx);
Assert.Equal(result, pluginOutput["is-found"]);
}
}
30 changes: 30 additions & 0 deletions tests/Plugin.AzDevOps.Tests/Plugin.AzDevOps.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageReference Include="xunit" Version="2.5.3"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3"/>
<PackageReference Include="Moq" Version="4.20.70" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Nox.Cli.Plugins\Nox.Cli.Plugin.AzDevOps\Nox.Cli.Plugin.AzDevOps.csproj" />
<ProjectReference Include="..\..\src\Nox.Cli.Variables\Nox.Cli.Variables.csproj" />
<ProjectReference Include="..\..\src\Nox.Cli\Nox.Cli.csproj" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion tests/Plugin.Core.Tests/SnakeNameTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Plugin.Core.Tests;

public class SnakeNameTests
{
[Theory]
[Theory (Skip = "This test can only be run locally as it requires a DevOps PAT")]
[InlineData("Hello.World", "hello_world")]
[InlineData("HelloWorld", "helloworld")]
[InlineData("HELLO.World", "hello_world")]
Expand Down

0 comments on commit ac614ce

Please sign in to comment.