diff --git a/.gitignore b/.gitignore index 4184f938..db81db57 100644 --- a/.gitignore +++ b/.gitignore @@ -105,4 +105,7 @@ tools/* coverage-results/* # Rider -**/.idea/* \ No newline at end of file +**/.idea/* + +# macOS +.DS_Store \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 0c1ea17b..77504b07 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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", + }, } diff --git a/Octokit.Reactive/Clients/IObservableMetaClient.cs b/Octokit.Reactive/Clients/IObservableMetaClient.cs index 97d63d19..35556703 100644 --- a/Octokit.Reactive/Clients/IObservableMetaClient.cs +++ b/Octokit.Reactive/Clients/IObservableMetaClient.cs @@ -10,6 +10,11 @@ namespace Octokit.Reactive /// public interface IObservableMetaClient { + /// + /// Returns a client to get public keys for validating request signatures. + /// + IObservablePublicKeysClient PublicKeys { get; } + /// /// Retrieves information about GitHub.com, the service or a GitHub Enterprise installation. /// diff --git a/Octokit.Reactive/Clients/IObservablePublicKeysClient.cs b/Octokit.Reactive/Clients/IObservablePublicKeysClient.cs new file mode 100644 index 00000000..02aae89a --- /dev/null +++ b/Octokit.Reactive/Clients/IObservablePublicKeysClient.cs @@ -0,0 +1,20 @@ +using System; + +namespace Octokit.Reactive +{ + /// + /// A client for GitHub's meta public keys API. + /// + /// + /// See the Secret scanning documentation for more details. + /// + public interface IObservablePublicKeysClient + { + /// + /// Retrieves public keys for validating request signatures. + /// + /// Thrown when a general API error occurs. + /// An containing public keys for validating request signatures. + IObservable Get(PublicKeyType keysType); + } +} diff --git a/Octokit.Reactive/Clients/ObservableMetaClient.cs b/Octokit.Reactive/Clients/ObservableMetaClient.cs index 3df88872..d8283ed6 100644 --- a/Octokit.Reactive/Clients/ObservableMetaClient.cs +++ b/Octokit.Reactive/Clients/ObservableMetaClient.cs @@ -18,9 +18,16 @@ namespace Octokit.Reactive { Ensure.ArgumentNotNull(client, nameof(client)); + PublicKeys = new ObservablePublicKeysClient(client); + _client = client.Meta; } + /// + /// Returns a client to manage get public keys for validating request signatures. + /// + public IObservablePublicKeysClient PublicKeys { get; private set; } + /// /// Retrieves information about GitHub.com, the service or a GitHub Enterprise installation. /// diff --git a/Octokit.Reactive/Clients/ObservablePublicKeysClient.cs b/Octokit.Reactive/Clients/ObservablePublicKeysClient.cs new file mode 100644 index 00000000..53396455 --- /dev/null +++ b/Octokit.Reactive/Clients/ObservablePublicKeysClient.cs @@ -0,0 +1,34 @@ +using System; +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; + +namespace Octokit.Reactive +{ + /// + /// A client for GitHub's public keys API. + /// + /// + /// See the Secret scanning documentation for more details. + /// + public class ObservablePublicKeysClient : IObservablePublicKeysClient + { + private readonly IPublicKeysClient _client; + + public ObservablePublicKeysClient(IGitHubClient client) + { + Ensure.ArgumentNotNull(client, nameof(client)); + + _client = client.Meta.PublicKeys; + } + + /// + /// Retrieves public keys for validating request signatures. + /// + /// Thrown when a general API error occurs. + /// An containing public keys for validating request signatures. + public IObservable Get(PublicKeyType keysType) + { + return _client.Get(keysType).ToObservable(); + } + } +} diff --git a/Octokit.Tests.Integration/Clients/PublicKeysClientTest.cs b/Octokit.Tests.Integration/Clients/PublicKeysClientTest.cs new file mode 100644 index 00000000..a6b2effb --- /dev/null +++ b/Octokit.Tests.Integration/Clients/PublicKeysClientTest.cs @@ -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); + } + } + } +} diff --git a/Octokit.Tests/Clients/PublicKeysClientTests.cs b/Octokit.Tests/Clients/PublicKeysClientTests.cs new file mode 100644 index 00000000..ee416a66 --- /dev/null +++ b/Octokit.Tests/Clients/PublicKeysClientTests.cs @@ -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(() => new PublicKeysClient(null)); + } + } + + public class TheGetMethod + { + [Fact] + public async Task RequestsTheCorrectUrl() + { + var connection = Substitute.For(); + var client = new PublicKeysClient(connection); + + await client.Get(PublicKeyType.CopilotApi); + + connection.Received() + .Get(Arg.Is(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(); + apiConnection.Get(Arg.Is(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(Arg.Is(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(); + apiConnection.Get(Arg.Is(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(Arg.Is(u => u.ToString() == "meta/public_keys/secret_scanning")); + } + } + } +} diff --git a/Octokit.Tests/Models/MetaPublicKeysTests.cs b/Octokit.Tests/Models/MetaPublicKeysTests.cs new file mode 100644 index 00000000..9e5f64ea --- /dev/null +++ b/Octokit.Tests/Models/MetaPublicKeysTests.cs @@ -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(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); + } + } +} diff --git a/Octokit.Tests/Reactive/ObservablePublicKeysClientTests.cs b/Octokit.Tests/Reactive/ObservablePublicKeysClientTests.cs new file mode 100644 index 00000000..14939de5 --- /dev/null +++ b/Octokit.Tests/Reactive/ObservablePublicKeysClientTests.cs @@ -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(); + 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(() => new ObservablePublicKeysClient((IGitHubClient)null)); + } + } + } +} diff --git a/Octokit/Clients/IMetaClient.cs b/Octokit/Clients/IMetaClient.cs index f4c21714..9c790060 100644 --- a/Octokit/Clients/IMetaClient.cs +++ b/Octokit/Clients/IMetaClient.cs @@ -10,6 +10,11 @@ namespace Octokit /// public interface IMetaClient { + /// + /// Returns a client to get public keys for validating request signatures. + /// + IPublicKeysClient PublicKeys { get; } + /// /// Retrieves information about GitHub.com, the service or a GitHub Enterprise installation. /// diff --git a/Octokit/Clients/IPublicKeysClient.cs b/Octokit/Clients/IPublicKeysClient.cs new file mode 100644 index 00000000..eae756c8 --- /dev/null +++ b/Octokit/Clients/IPublicKeysClient.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace Octokit +{ + /// + /// A client for GitHub's meta public keys API. + /// + /// + /// See the Secret scanning documentation for more details. + /// + public interface IPublicKeysClient + { + /// + /// Retrieves public keys for validating request signatures. + /// + /// Thrown when a general API error occurs. + /// An containing public keys for validating request signatures. + Task Get(PublicKeyType keysType); + } +} diff --git a/Octokit/Clients/MetaClient.cs b/Octokit/Clients/MetaClient.cs index c683a34b..52a61f14 100644 --- a/Octokit/Clients/MetaClient.cs +++ b/Octokit/Clients/MetaClient.cs @@ -18,8 +18,14 @@ namespace Octokit public MetaClient(IApiConnection apiConnection) : base(apiConnection) { + PublicKeys = new PublicKeysClient(apiConnection); } + /// + /// Returns a client to manage get public keys for validating request signatures. + /// + public IPublicKeysClient PublicKeys { get; private set; } + /// /// Retrieves information about GitHub.com, the service or a GitHub Enterprise installation. /// diff --git a/Octokit/Clients/PublicKeysClient.cs b/Octokit/Clients/PublicKeysClient.cs new file mode 100644 index 00000000..cc34141f --- /dev/null +++ b/Octokit/Clients/PublicKeysClient.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; + +namespace Octokit +{ + /// + /// A client for GitHub's public keys API. + /// + /// + /// See the Secret scanning documentation for more details. + /// + public class PublicKeysClient : ApiClient, IPublicKeysClient + { + /// + /// Initializes a new GitHub Meta Public Keys API client. + /// + /// An API connection. + public PublicKeysClient(IApiConnection apiConnection) + : base(apiConnection) + { + } + + /// + /// Retrieves public keys for validating request signatures. + /// + /// Thrown when a general API error occurs. + /// An containing public keys for validating request signatures. + [ManualRoute("GET", "/meta/public_keys/{keysType}")] + public Task Get(PublicKeyType keysType) + { + return ApiConnection.Get(ApiUrls.PublicKeys(keysType)); + } + } +} diff --git a/Octokit/Helpers/ApiUrls.cs b/Octokit/Helpers/ApiUrls.cs index 62c752a5..b70ae025 100644 --- a/Octokit/Helpers/ApiUrls.cs +++ b/Octokit/Helpers/ApiUrls.cs @@ -4833,6 +4833,16 @@ namespace Octokit return "meta".FormatUri(); } + /// + /// Returns the that returns meta in + /// response to a GET request. + /// + /// The to meta. + public static Uri PublicKeys(PublicKeyType keysType) + { + return "meta/public_keys/{0}".FormatUri(keysType.ToParameter()); + } + /// /// Returns the that returns all organization credentials in /// response to a GET request. diff --git a/Octokit/Models/Common/PublicKeyType.cs b/Octokit/Models/Common/PublicKeyType.cs new file mode 100644 index 00000000..3a8c09a6 --- /dev/null +++ b/Octokit/Models/Common/PublicKeyType.cs @@ -0,0 +1,19 @@ +using Octokit.Internal; + +namespace Octokit +{ + public enum PublicKeyType + { + /// + /// Copilot API public keys for validating request signatures + /// + [Parameter(Value = "copilot_api")] + CopilotApi, + + /// + /// Secret scanning public keys for validating request signatures + /// + [Parameter(Value = "secret_scanning")] + SecretScanning + } +} diff --git a/Octokit/Models/Response/MetaPublicKey.cs b/Octokit/Models/Response/MetaPublicKey.cs new file mode 100644 index 00000000..05b6a86c --- /dev/null +++ b/Octokit/Models/Response/MetaPublicKey.cs @@ -0,0 +1,29 @@ +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class MetaPublicKey + { + public MetaPublicKey() { } + + public MetaPublicKey(string keyIdentifier, string key, bool isCurrent) + { + KeyIdentifier = keyIdentifier; + Key = key; + IsCurrent = isCurrent; + } + + public string KeyIdentifier { get; protected set; } + + public string Key { get; protected set; } + + public bool IsCurrent { get; protected set; } + + internal string DebuggerDisplay + { + get { return string.Format(CultureInfo.InvariantCulture, "KeyIdentifier: {0} IsCurrent: {1}", KeyIdentifier, IsCurrent); } + } + } +} diff --git a/Octokit/Models/Response/MetaPublicKeys.cs b/Octokit/Models/Response/MetaPublicKeys.cs new file mode 100644 index 00000000..7927ab7a --- /dev/null +++ b/Octokit/Models/Response/MetaPublicKeys.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class MetaPublicKeys + { + public MetaPublicKeys() { } + + public MetaPublicKeys(IReadOnlyList publicKeys) + { + PublicKeys = publicKeys; + } + + public IReadOnlyList PublicKeys { get; protected set; } + + internal string DebuggerDisplay + { + get { return string.Format(CultureInfo.InvariantCulture, "PublicKeys: {0}", PublicKeys.Count); } + } + } +}