Skip to content

Commit

Permalink
[FEAT]: Add support for Public Keys API (#2945)
Browse files Browse the repository at this point in the history
* Add support for /mets/public_keys/<keyType>

* "files.insertFinalNewline": false

* revert and make setttings.json change csharp only

* formatting

* remove final new line

---------

Co-authored-by: Nick Floyd <139819+nickfloyd@users.noreply.github.com>
  • Loading branch information
colbylwilliams and nickfloyd authored Dec 27, 2024
1 parent 6eefc59 commit f9fb116
Show file tree
Hide file tree
Showing 18 changed files with 418 additions and 2 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,7 @@ tools/*
coverage-results/*

# Rider
**/.idea/*
**/.idea/*

# macOS
.DS_Store
8 changes: 7 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,11 @@
"files.insertFinalNewline": true,
"editor.detectIndentation": false,
"editor.tabSize": 2,
"editor.insertSpaces": true
"editor.insertSpaces": true,
"[csharp]": {
"editor.tabSize": 4
},
"explorer.fileNesting.patterns": {
"*.cs": "I${capture}.cs",
},
}
5 changes: 5 additions & 0 deletions Octokit.Reactive/Clients/IObservableMetaClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ namespace Octokit.Reactive
/// </remarks>
public interface IObservableMetaClient
{
/// <summary>
/// Returns a client to get public keys for validating request signatures.
/// </summary>
IObservablePublicKeysClient PublicKeys { get; }

/// <summary>
/// Retrieves information about GitHub.com, the service or a GitHub Enterprise installation.
/// </summary>
Expand Down
20 changes: 20 additions & 0 deletions Octokit.Reactive/Clients/IObservablePublicKeysClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;

namespace Octokit.Reactive
{
/// <summary>
/// A client for GitHub's meta public keys API.
/// </summary>
/// <remarks>
/// See the <a href="https://docs.github.com/code-security/secret-scanning/secret-scanning-partner-program#implement-signature-verification-in-your-secret-alert-service">Secret scanning documentation</a> for more details.
/// </remarks>
public interface IObservablePublicKeysClient
{
/// <summary>
/// Retrieves public keys for validating request signatures.
/// </summary>
/// <exception cref="ApiException">Thrown when a general API error occurs.</exception>
/// <returns>An <see cref="MetaPublicKeys"/> containing public keys for validating request signatures.</returns>
IObservable<MetaPublicKeys> Get(PublicKeyType keysType);
}
}
7 changes: 7 additions & 0 deletions Octokit.Reactive/Clients/ObservableMetaClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,16 @@ public ObservableMetaClient(IGitHubClient client)
{
Ensure.ArgumentNotNull(client, nameof(client));

PublicKeys = new ObservablePublicKeysClient(client);

_client = client.Meta;
}

/// <summary>
/// Returns a client to manage get public keys for validating request signatures.
/// </summary>
public IObservablePublicKeysClient PublicKeys { get; private set; }

/// <summary>
/// Retrieves information about GitHub.com, the service or a GitHub Enterprise installation.
/// </summary>
Expand Down
34 changes: 34 additions & 0 deletions Octokit.Reactive/Clients/ObservablePublicKeysClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;

namespace Octokit.Reactive
{
/// <summary>
/// A client for GitHub's public keys API.
/// </summary>
/// <remarks>
/// See the <a href="https://docs.github.com/code-security/secret-scanning/secret-scanning-partner-program#implement-signature-verification-in-your-secret-alert-service">Secret scanning documentation</a> for more details.
/// </remarks>
public class ObservablePublicKeysClient : IObservablePublicKeysClient
{
private readonly IPublicKeysClient _client;

public ObservablePublicKeysClient(IGitHubClient client)
{
Ensure.ArgumentNotNull(client, nameof(client));

_client = client.Meta.PublicKeys;
}

/// <summary>
/// Retrieves public keys for validating request signatures.
/// </summary>
/// <exception cref="ApiException">Thrown when a general API error occurs.</exception>
/// <returns>An <see cref="MetaPublicKeys"/> containing public keys for validating request signatures.</returns>
public IObservable<MetaPublicKeys> Get(PublicKeyType keysType)
{
return _client.Get(keysType).ToObservable();
}
}
}
28 changes: 28 additions & 0 deletions Octokit.Tests.Integration/Clients/PublicKeysClientTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Threading.Tasks;
using Xunit;

namespace Octokit.Tests.Integration.Clients
{
public class PublicKeysClientTests
{
public class TheGetMethod
{
[IntegrationTest]
public async Task CanRetrievePublicKeys()
{
var github = Helper.GetAnonymousClient();

var result = await github.Meta.PublicKeys.Get(PublicKeyType.SecretScanning);

Assert.NotNull(result);
Assert.Equal(2, result.PublicKeys.Count);

Assert.NotNull(result.PublicKeys[0].KeyIdentifier);
Assert.NotNull(result.PublicKeys[0].Key);

Assert.NotNull(result.PublicKeys[1].KeyIdentifier);
Assert.NotNull(result.PublicKeys[1].Key);
}
}
}
}
90 changes: 90 additions & 0 deletions Octokit.Tests/Clients/PublicKeysClientTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System;
using System.Threading.Tasks;
using NSubstitute;
using Xunit;

namespace Octokit.Tests.Clients
{
public class PublicKeysClientTests
{
public class TheCtor
{
[Fact]
public void EnsuresNonNullArguments()
{
Assert.Throws<ArgumentNullException>(() => new PublicKeysClient(null));
}
}

public class TheGetMethod
{
[Fact]
public async Task RequestsTheCorrectUrl()
{
var connection = Substitute.For<IApiConnection>();
var client = new PublicKeysClient(connection);

await client.Get(PublicKeyType.CopilotApi);

connection.Received()
.Get<MetaPublicKeys>(Arg.Is<Uri>(u => u.ToString() == "meta/public_keys/copilot_api"));
}

[Fact]
public async Task RequestsCopilotApiPublicKeysEndpoint()
{
var publicKeys = new MetaPublicKeys(publicKeys: new[] {
new MetaPublicKey("4fe6b016179b74078ade7581abf4e84fb398c6fae4fb973972235b84fcd70ca3", "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELPuPiLVQbHY/clvpNnY+0BzYIXgo\nS0+XhEkTWUZEEznIVpS3rQseDTG6//gEWr4j9fY35+dGOxwOx3Z9mK3i7w==\n-----END PUBLIC KEY-----\n", true),
new MetaPublicKey("df3454252d91570ae1bc597182d1183c7a8d42ff0ae96e0f2be4ba278d776546", "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEl5xbyr5bmETCJzqAvDnYl1ZKJrkf\n89Nyq5j06TTKrnHXXDw4FYNY1uF2S/w6EOaxbf9BxOidCLvjJ8ZgKzNpww==\n-----END PUBLIC KEY-----\n", false)
});

var apiConnection = Substitute.For<IApiConnection>();
apiConnection.Get<MetaPublicKeys>(Arg.Is<Uri>(u => u.ToString() == "meta/public_keys/copilot_api")).Returns(Task.FromResult(publicKeys));

var client = new PublicKeysClient(apiConnection);

var result = await client.Get(PublicKeyType.CopilotApi);

Assert.Equal(2, result.PublicKeys.Count);
Assert.Equal("4fe6b016179b74078ade7581abf4e84fb398c6fae4fb973972235b84fcd70ca3", result.PublicKeys[0].KeyIdentifier);
Assert.Equal("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELPuPiLVQbHY/clvpNnY+0BzYIXgo\nS0+XhEkTWUZEEznIVpS3rQseDTG6//gEWr4j9fY35+dGOxwOx3Z9mK3i7w==\n-----END PUBLIC KEY-----\n", result.PublicKeys[0].Key);
Assert.True(result.PublicKeys[0].IsCurrent);

Assert.Equal("df3454252d91570ae1bc597182d1183c7a8d42ff0ae96e0f2be4ba278d776546", result.PublicKeys[1].KeyIdentifier);
Assert.Equal("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEl5xbyr5bmETCJzqAvDnYl1ZKJrkf\n89Nyq5j06TTKrnHXXDw4FYNY1uF2S/w6EOaxbf9BxOidCLvjJ8ZgKzNpww==\n-----END PUBLIC KEY-----\n", result.PublicKeys[1].Key);
Assert.False(result.PublicKeys[1].IsCurrent);

apiConnection.Received()
.Get<MetaPublicKeys>(Arg.Is<Uri>(u => u.ToString() == "meta/public_keys/copilot_api"));
}

[Fact]
public async Task RequestSecretScanningPublicKeysEndpoint()
{
var publicKeys = new MetaPublicKeys(publicKeys: new[] {
new MetaPublicKey("90a421169f0a406205f1563a953312f0be898d3c7b6c06b681aa86a874555f4a", "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9MJJHnMfn2+H4xL4YaPDA4RpJqUq\nkCmRCBnYERxZanmcpzQSXs1X/AljlKkbJ8qpVIW4clayyef9gWhFbNHWAA==\n-----END PUBLIC KEY-----\n", false),
new MetaPublicKey("bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c", "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYAGMWO8XgCamYKMJS6jc/qgvSlAd\nAjPuDPRcXU22YxgBrz+zoN19MzuRyW87qEt9/AmtoNP5GrobzUvQSyJFVw==\n-----END PUBLIC KEY-----\n", true)
});

var apiConnection = Substitute.For<IApiConnection>();
apiConnection.Get<MetaPublicKeys>(Arg.Is<Uri>(u => u.ToString() == "meta/public_keys/secret_scanning")).Returns(Task.FromResult(publicKeys));

var client = new PublicKeysClient(apiConnection);

var result = await client.Get(PublicKeyType.SecretScanning);

Assert.Equal(2, result.PublicKeys.Count);
Assert.Equal("90a421169f0a406205f1563a953312f0be898d3c7b6c06b681aa86a874555f4a", result.PublicKeys[0].KeyIdentifier);
Assert.Equal("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9MJJHnMfn2+H4xL4YaPDA4RpJqUq\nkCmRCBnYERxZanmcpzQSXs1X/AljlKkbJ8qpVIW4clayyef9gWhFbNHWAA==\n-----END PUBLIC KEY-----\n", result.PublicKeys[0].Key);
Assert.False(result.PublicKeys[0].IsCurrent);

Assert.Equal("bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c", result.PublicKeys[1].KeyIdentifier);
Assert.Equal("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYAGMWO8XgCamYKMJS6jc/qgvSlAd\nAjPuDPRcXU22YxgBrz+zoN19MzuRyW87qEt9/AmtoNP5GrobzUvQSyJFVw==\n-----END PUBLIC KEY-----\n", result.PublicKeys[1].Key);
Assert.True(result.PublicKeys[1].IsCurrent);

apiConnection.Received()
.Get<MetaPublicKeys>(Arg.Is<Uri>(u => u.ToString() == "meta/public_keys/secret_scanning"));
}
}
}
}
44 changes: 44 additions & 0 deletions Octokit.Tests/Models/MetaPublicKeysTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Octokit.Internal;
using Xunit;

namespace Octokit.Tests.Models
{
public class MetaPublicKeysTests
{
[Fact]
public void CanBeDeserialized()
{
const string json = @"{
""public_keys"": [
{
""key_identifier"": ""90a421169f0a406205f1563a953312f0be898d3c7b6c06b681aa86a874555f4a"",
""key"": ""-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9MJJHnMfn2+H4xL4YaPDA4RpJqUq\nkCmRCBnYERxZanmcpzQSXs1X/AljlKkbJ8qpVIW4clayyef9gWhFbNHWAA==\n-----END PUBLIC KEY-----\n"",
""is_current"": false
},
{
""key_identifier"": ""bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c"",
""key"": ""-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYAGMWO8XgCamYKMJS6jc/qgvSlAd\nAjPuDPRcXU22YxgBrz+zoN19MzuRyW87qEt9/AmtoNP5GrobzUvQSyJFVw==\n-----END PUBLIC KEY-----\n"",
""is_current"": true
}
]
}
";
var serializer = new SimpleJsonSerializer();

var keys = serializer.Deserialize<MetaPublicKeys>(json);

Assert.NotNull(keys);
Assert.Equal(2, keys.PublicKeys.Count);

var key1 = keys.PublicKeys[0];
Assert.Equal("90a421169f0a406205f1563a953312f0be898d3c7b6c06b681aa86a874555f4a", key1.KeyIdentifier);
Assert.Equal("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9MJJHnMfn2+H4xL4YaPDA4RpJqUq\nkCmRCBnYERxZanmcpzQSXs1X/AljlKkbJ8qpVIW4clayyef9gWhFbNHWAA==\n-----END PUBLIC KEY-----\n", key1.Key);
Assert.False(key1.IsCurrent);

var key2 = keys.PublicKeys[1];
Assert.Equal("bcb53661c06b4728e59d897fb6165d5c9cda0fd9cdf9d09ead458168deb7518c", key2.KeyIdentifier);
Assert.Equal("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYAGMWO8XgCamYKMJS6jc/qgvSlAd\nAjPuDPRcXU22YxgBrz+zoN19MzuRyW87qEt9/AmtoNP5GrobzUvQSyJFVw==\n-----END PUBLIC KEY-----\n", key2.Key);
Assert.True(key2.IsCurrent);
}
}
}
33 changes: 33 additions & 0 deletions Octokit.Tests/Reactive/ObservablePublicKeysClientTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using NSubstitute;
using Octokit.Reactive;
using Xunit;

namespace Octokit.Tests.Reactive
{
public class ObservablePublicKeysClientTests
{
public class TheGetMethod
{
[Fact]
public void CallsIntoClient()
{
var gitHubClient = Substitute.For<IGitHubClient>();
var client = new ObservablePublicKeysClient(gitHubClient);

client.Get(PublicKeyType.SecretScanning);

gitHubClient.Meta.PublicKeys.Received(1).Get(PublicKeyType.SecretScanning);
}
}

public class TheCtor
{
[Fact]
public void EnsuresNonNullArguments()
{
Assert.Throws<ArgumentNullException>(() => new ObservablePublicKeysClient((IGitHubClient)null));
}
}
}
}
5 changes: 5 additions & 0 deletions Octokit/Clients/IMetaClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ namespace Octokit
/// </remarks>
public interface IMetaClient
{
/// <summary>
/// Returns a client to get public keys for validating request signatures.
/// </summary>
IPublicKeysClient PublicKeys { get; }

/// <summary>
/// Retrieves information about GitHub.com, the service or a GitHub Enterprise installation.
/// </summary>
Expand Down
20 changes: 20 additions & 0 deletions Octokit/Clients/IPublicKeysClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Threading.Tasks;

namespace Octokit
{
/// <summary>
/// A client for GitHub's meta public keys API.
/// </summary>
/// <remarks>
/// See the <a href="https://docs.github.com/code-security/secret-scanning/secret-scanning-partner-program#implement-signature-verification-in-your-secret-alert-service">Secret scanning documentation</a> for more details.
/// </remarks>
public interface IPublicKeysClient
{
/// <summary>
/// Retrieves public keys for validating request signatures.
/// </summary>
/// <exception cref="ApiException">Thrown when a general API error occurs.</exception>
/// <returns>An <see cref="MetaPublicKeys"/> containing public keys for validating request signatures.</returns>
Task<MetaPublicKeys> Get(PublicKeyType keysType);
}
}
6 changes: 6 additions & 0 deletions Octokit/Clients/MetaClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@ public class MetaClient : ApiClient, IMetaClient
public MetaClient(IApiConnection apiConnection)
: base(apiConnection)
{
PublicKeys = new PublicKeysClient(apiConnection);
}

/// <summary>
/// Returns a client to manage get public keys for validating request signatures.
/// </summary>
public IPublicKeysClient PublicKeys { get; private set; }

/// <summary>
/// Retrieves information about GitHub.com, the service or a GitHub Enterprise installation.
/// </summary>
Expand Down
33 changes: 33 additions & 0 deletions Octokit/Clients/PublicKeysClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Threading.Tasks;

namespace Octokit
{
/// <summary>
/// A client for GitHub's public keys API.
/// </summary>
/// <remarks>
/// See the <a href="https://docs.github.com/code-security/secret-scanning/secret-scanning-partner-program#implement-signature-verification-in-your-secret-alert-service">Secret scanning documentation</a> for more details.
/// </remarks>
public class PublicKeysClient : ApiClient, IPublicKeysClient
{
/// <summary>
/// Initializes a new GitHub Meta Public Keys API client.
/// </summary>
/// <param name="apiConnection">An API connection.</param>
public PublicKeysClient(IApiConnection apiConnection)
: base(apiConnection)
{
}

/// <summary>
/// Retrieves public keys for validating request signatures.
/// </summary>
/// <exception cref="ApiException">Thrown when a general API error occurs.</exception>
/// <returns>An <see cref="MetaPublicKeys"/> containing public keys for validating request signatures.</returns>
[ManualRoute("GET", "/meta/public_keys/{keysType}")]
public Task<MetaPublicKeys> Get(PublicKeyType keysType)
{
return ApiConnection.Get<MetaPublicKeys>(ApiUrls.PublicKeys(keysType));
}
}
}
Loading

0 comments on commit f9fb116

Please sign in to comment.