diff --git a/src/Nitro/CommandLine/src/CommandLine/Directory.Build.targets b/src/Nitro/CommandLine/src/CommandLine/Directory.Build.targets index 5673185eaf3..8e56ebe7b00 100644 --- a/src/Nitro/CommandLine/src/CommandLine/Directory.Build.targets +++ b/src/Nitro/CommandLine/src/CommandLine/Directory.Build.targets @@ -9,6 +9,7 @@ <_NitroIdentityClientId>$(NitroIdentityClientId) <_NitroIdentityScopes>$(NitroIdentityScopes) + <_NitroApiClientId>$(NitroApiClientId) @@ -23,6 +24,7 @@ namespace $(_SecretsNamespace) { internal const string NitroIdentityClientId = "$(_NitroIdentityClientId)"; internal const string NitroIdentityScopes = "$(_NitroIdentityScopes)"; + internal const string NitroApiClientId = "$(_NitroApiClientId)"; } } ))" /> diff --git a/src/Nitro/CommandLine/src/CommandLine/Services/NitroClientContext.cs b/src/Nitro/CommandLine/src/CommandLine/Services/NitroClientContext.cs index e884528e0fc..1c2bfd7740c 100644 --- a/src/Nitro/CommandLine/src/CommandLine/Services/NitroClientContext.cs +++ b/src/Nitro/CommandLine/src/CommandLine/Services/NitroClientContext.cs @@ -16,6 +16,8 @@ public Uri Url public INitroClientAuthorization? Authorization { get; private set; } + public string? ClientId => BuildSecrets.NitroApiClientId; + public void Configure(string? apiUrl, INitroClientAuthorization? authorization) { if (string.IsNullOrWhiteSpace(apiUrl)) diff --git a/src/Nitro/CommandLine/test/CommandLine.Tests/GlobalOptionsTests.cs b/src/Nitro/CommandLine/test/CommandLine.Tests/GlobalOptionsTests.cs index e7a3871a3e9..9b56ccdfb12 100644 --- a/src/Nitro/CommandLine/test/CommandLine.Tests/GlobalOptionsTests.cs +++ b/src/Nitro/CommandLine/test/CommandLine.Tests/GlobalOptionsTests.cs @@ -67,6 +67,57 @@ public async Task ExecuteAsync_Should_PreferApiKey_Over_SessionToken() Assert.False(client.DefaultRequestHeaders.Contains("Authorization")); } + [Fact] + public async Task ExecuteAsync_Should_ConfigureApiKeyAuth_When_EnvApiKeySet() + { + // arrange + var envProvider = CreateEnvironmentProvider(apiKey: "env-key"); + + // act + await using var provider = await BuildAndExecuteAsync([], environmentVariables: envProvider); + using var client = CreateApiClient(provider); + + // assert + var apiKeyHeader = Assert.Single(client.DefaultRequestHeaders.GetValues("CCC-api-key")); + Assert.Equal("env-key", apiKeyHeader); + Assert.False(client.DefaultRequestHeaders.Contains("Authorization")); + } + + [Fact] + public async Task ExecuteAsync_Should_PreferEnvApiKey_Over_SessionToken() + { + // arrange + var session = CreateSessionWithTokens(accessToken: "session-token"); + var envProvider = CreateEnvironmentProvider(apiKey: "env-key"); + + // act + await using var provider = await BuildAndExecuteAsync( + [], session, environmentVariables: envProvider); + using var client = CreateApiClient(provider); + + // assert + var apiKeyHeader = Assert.Single(client.DefaultRequestHeaders.GetValues("CCC-api-key")); + Assert.Equal("env-key", apiKeyHeader); + Assert.False(client.DefaultRequestHeaders.Contains("Authorization")); + } + + [Fact] + public async Task ExecuteAsync_Should_PreferExplicitApiKey_Over_EnvApiKey() + { + // arrange + var envProvider = CreateEnvironmentProvider(apiKey: "env-key"); + + // act + await using var provider = await BuildAndExecuteAsync( + ["--api-key", "cli-key"], environmentVariables: envProvider); + using var client = CreateApiClient(provider); + + // assert + var apiKeyHeader = Assert.Single(client.DefaultRequestHeaders.GetValues("CCC-api-key")); + Assert.Equal("cli-key", apiKeyHeader); + Assert.False(client.DefaultRequestHeaders.Contains("Authorization")); + } + [Theory] [InlineData("custom.host.com", "https://custom.host.com/graphql")] [InlineData("https://custom.host.com", "https://custom.host.com/graphql")] @@ -239,6 +290,13 @@ private static HttpClient CreateApiClient(ServiceProvider provider) return factory.CreateClient(ApiClient.ClientName); } + private static IEnvironmentVariableProvider CreateEnvironmentProvider(string? apiKey = null) + { + var mock = new Mock(); + mock.Setup(x => x.GetEnvironmentVariable("NITRO_API_KEY")).Returns(apiKey); + return mock.Object; + } + private static Session CreateSessionWithTokens( string apiUrl = "api.chillicream.com", string accessToken = "test-access-token") diff --git a/src/Nitro/Common/src/ChilliCream.Nitro.Client/Extensions/NitroClientServiceCollectionExtensions.cs b/src/Nitro/Common/src/ChilliCream.Nitro.Client/Extensions/NitroClientServiceCollectionExtensions.cs index 8ec17671292..01ce3fe1648 100644 --- a/src/Nitro/Common/src/ChilliCream.Nitro.Client/Extensions/NitroClientServiceCollectionExtensions.cs +++ b/src/Nitro/Common/src/ChilliCream.Nitro.Client/Extensions/NitroClientServiceCollectionExtensions.cs @@ -195,6 +195,12 @@ private static void ConfigureApiHttpClient(IServiceProvider sp, HttpClient clien var provider = sp.GetRequiredService(); client.BaseAddress = provider.Url; + if (!string.IsNullOrWhiteSpace(provider.ClientId)) + { + client.DefaultRequestHeaders.Remove(NitroClientHeaders.GraphQLClientId); + client.DefaultRequestHeaders.Add(NitroClientHeaders.GraphQLClientId, provider.ClientId); + } + switch (provider.Authorization) { case NitroClientApiKeyAuthorization apiKey: diff --git a/src/Nitro/Common/src/ChilliCream.Nitro.Client/INitroClientContextProvider.cs b/src/Nitro/Common/src/ChilliCream.Nitro.Client/INitroClientContextProvider.cs index 9e83b65e6fd..01a1c6bb933 100644 --- a/src/Nitro/Common/src/ChilliCream.Nitro.Client/INitroClientContextProvider.cs +++ b/src/Nitro/Common/src/ChilliCream.Nitro.Client/INitroClientContextProvider.cs @@ -5,4 +5,6 @@ public interface INitroClientContextProvider Uri Url { get; } INitroClientAuthorization? Authorization { get; } + + string? ClientId { get; } } diff --git a/src/Nitro/Common/src/ChilliCream.Nitro.Client/NitroClientHeaders.cs b/src/Nitro/Common/src/ChilliCream.Nitro.Client/NitroClientHeaders.cs index 0d2ceca48de..d2f98a90994 100644 --- a/src/Nitro/Common/src/ChilliCream.Nitro.Client/NitroClientHeaders.cs +++ b/src/Nitro/Common/src/ChilliCream.Nitro.Client/NitroClientHeaders.cs @@ -2,6 +2,7 @@ namespace ChilliCream.Nitro.Client; public static class NitroClientHeaders { + public const string GraphQLClientId = "GraphQL-Client-Id"; public const string GraphQLClientVersion = "GraphQL-Client-Version"; public const string GraphQLPreflight = "GraphQL-Preflight"; public const string CccAgent = "ccc-agent";