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

- added Verify Group feature to AzDevOpsVerifyAadGroups_v1.cs plugin #259

Merged
merged 2 commits into from
Jul 10, 2024
Merged
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
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
Loading