From 704df1a75e94350439fa63c152d4bbade866369b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 7 Dec 2020 12:28:09 +0000 Subject: [PATCH] Implement grant API key (#5142) (#5154) Contributes to #5096 Co-authored-by: Steve Gordon --- src/Nest/Descriptors.Security.cs | 10 + src/Nest/ElasticClient.Security.cs | 24 +++ src/Nest/Requests.Security.cs | 23 +++ .../Security/ApiKey/GrantApiKey/ApiKey.cs | 68 ++++++ .../ApiKey/GrantApiKey/GrantApiKeyRequest.cs | 99 +++++++++ .../ApiKey/GrantApiKey/GrantApiKeyResponse.cs | 38 ++++ .../Security/ApiKey/GrantApiKey/GrantType.cs | 16 ++ .../_Generated/ApiUrlsLookup.generated.cs | 1 + .../ApiKey/GrantApiKey/GrantApiKeyTests.cs | 195 ++++++++++++++++++ .../ApiKey/GrantApiKey/GrantApiKeyUrlTests.cs | 20 ++ 10 files changed, 494 insertions(+) create mode 100644 src/Nest/XPack/Security/ApiKey/GrantApiKey/ApiKey.cs create mode 100644 src/Nest/XPack/Security/ApiKey/GrantApiKey/GrantApiKeyRequest.cs create mode 100644 src/Nest/XPack/Security/ApiKey/GrantApiKey/GrantApiKeyResponse.cs create mode 100644 src/Nest/XPack/Security/ApiKey/GrantApiKey/GrantType.cs create mode 100644 tests/Tests/XPack/Security/ApiKey/GrantApiKey/GrantApiKeyTests.cs create mode 100644 tests/Tests/XPack/Security/ApiKey/GrantApiKey/GrantApiKeyUrlTests.cs diff --git a/src/Nest/Descriptors.Security.cs b/src/Nest/Descriptors.Security.cs index 62beaeebfa8..497ad9ea9aa 100644 --- a/src/Nest/Descriptors.Security.cs +++ b/src/Nest/Descriptors.Security.cs @@ -417,6 +417,16 @@ public partial class GetUserPrivilegesDescriptor : RequestDescriptorBaseDescriptor for GrantApiKey https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-grant-api-key.html + public partial class GrantApiKeyDescriptor : RequestDescriptorBase, IGrantApiKeyRequest + { + internal override ApiUrls ApiUrls => ApiUrlsLookups.SecurityGrantApiKey; + // values part of the url path + // Request parameters + ///If `true` (the default) then refresh the affected shards to make this operation visible to search, if `wait_for` then wait for a refresh to make this operation visible to search, if `false` then do nothing with refreshes. + public GrantApiKeyDescriptor Refresh(Refresh? refresh) => Qs("refresh", refresh); + } + ///Descriptor for HasPrivileges https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-has-privileges.html public partial class HasPrivilegesDescriptor : RequestDescriptorBase, IHasPrivilegesRequest { diff --git a/src/Nest/ElasticClient.Security.cs b/src/Nest/ElasticClient.Security.cs index 9a4d33abe63..abeacd0ad2d 100644 --- a/src/Nest/ElasticClient.Security.cs +++ b/src/Nest/ElasticClient.Security.cs @@ -517,6 +517,30 @@ internal SecurityNamespace(ElasticClient client): base(client) /// public Task GetUserPrivilegesAsync(IGetUserPrivilegesRequest request, CancellationToken ct = default) => DoRequestAsync(request, request.RequestParameters, ct); /// + /// POST request to the security.grant_api_key API, read more about this API online: + /// + /// https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-grant-api-key.html + /// + public GrantApiKeyResponse GrantApiKey(Func selector) => GrantApiKey(selector.InvokeOrDefault(new GrantApiKeyDescriptor())); + /// + /// POST request to the security.grant_api_key API, read more about this API online: + /// + /// https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-grant-api-key.html + /// + public Task GrantApiKeyAsync(Func selector, CancellationToken ct = default) => GrantApiKeyAsync(selector.InvokeOrDefault(new GrantApiKeyDescriptor()), ct); + /// + /// POST request to the security.grant_api_key API, read more about this API online: + /// + /// https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-grant-api-key.html + /// + public GrantApiKeyResponse GrantApiKey(IGrantApiKeyRequest request) => DoRequest(request, request.RequestParameters); + /// + /// POST request to the security.grant_api_key API, read more about this API online: + /// + /// https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-grant-api-key.html + /// + public Task GrantApiKeyAsync(IGrantApiKeyRequest request, CancellationToken ct = default) => DoRequestAsync(request, request.RequestParameters, ct); + /// /// POST request to the security.has_privileges API, read more about this API online: /// /// https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-has-privileges.html diff --git a/src/Nest/Requests.Security.cs b/src/Nest/Requests.Security.cs index f8165668306..2464dbb1d27 100644 --- a/src/Nest/Requests.Security.cs +++ b/src/Nest/Requests.Security.cs @@ -708,6 +708,29 @@ public partial class GetUserPrivilegesRequest : PlainRequestBase + { + } + + ///Request for GrantApiKey https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-grant-api-key.html + public partial class GrantApiKeyRequest : PlainRequestBase, IGrantApiKeyRequest + { + protected IGrantApiKeyRequest Self => this; + internal override ApiUrls ApiUrls => ApiUrlsLookups.SecurityGrantApiKey; + // values part of the url path + // Request parameters + /// + /// If `true` (the default) then refresh the affected shards to make this operation visible to search, if `wait_for` then wait for a refresh + /// to make this operation visible to search, if `false` then do nothing with refreshes. + /// + public Refresh? Refresh + { + get => Q("refresh"); + set => Q("refresh", value); + } + } + [InterfaceDataContract] public partial interface IHasPrivilegesRequest : IRequest { diff --git a/src/Nest/XPack/Security/ApiKey/GrantApiKey/ApiKey.cs b/src/Nest/XPack/Security/ApiKey/GrantApiKey/ApiKey.cs new file mode 100644 index 00000000000..c55ca31f3b8 --- /dev/null +++ b/src/Nest/XPack/Security/ApiKey/GrantApiKey/ApiKey.cs @@ -0,0 +1,68 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Runtime.Serialization; +using Elasticsearch.Net.Utf8Json; + +namespace Nest +{ + [InterfaceDataContract] + [ReadAs(typeof(ApiKey))] + public interface IApiKey + { + /// + /// Optional expiration for the API key being generated. + /// If an expiration is not provided then the API keys do not expire. + /// + [DataMember(Name = "expiration")] + Time Expiration { get; set; } + + /// + /// A name for this API key. + /// + [DataMember(Name = "name")] + string Name { get; set; } + + /// + /// Optional role descriptors for this API key, if not provided then permissions of authenticated user are applied. + /// + [DataMember(Name = "role_descriptors")] + IApiKeyRoles Roles { get; set; } + } + + public class ApiKey : IApiKey + { + /// + public Time Expiration { get; set; } + + /// + public string Name { get; set; } + + /// + public IApiKeyRoles Roles { get; set; } + } + + public class ApiKeyDescriptor : DescriptorBase, IApiKey + { + /// + Time IApiKey.Expiration { get; set; } + + /// + string IApiKey.Name { get; set; } + + /// + IApiKeyRoles IApiKey.Roles { get; set; } + + /// + public ApiKeyDescriptor Expiration(Time expiration) => Assign(expiration, (a, v) => a.Expiration = v); + + /// + public ApiKeyDescriptor Name(string name) => Assign(name, (a, v) => a.Name = v); + + /// + public ApiKeyDescriptor Roles(Func> selector) => + Assign(selector, (a, v) => a.Roles = v.InvokeOrDefault(new ApiKeyRolesDescriptor()).Value); + } +} diff --git a/src/Nest/XPack/Security/ApiKey/GrantApiKey/GrantApiKeyRequest.cs b/src/Nest/XPack/Security/ApiKey/GrantApiKey/GrantApiKeyRequest.cs new file mode 100644 index 00000000000..0fec21afc25 --- /dev/null +++ b/src/Nest/XPack/Security/ApiKey/GrantApiKey/GrantApiKeyRequest.cs @@ -0,0 +1,99 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Runtime.Serialization; + +namespace Nest +{ + [MapsApi("security.grant_api_key.json")] + [ReadAs(typeof(GrantApiKeyRequest))] + public partial interface IGrantApiKeyRequest + { + /// + /// The user’s access token. If you specify the access_token grant type, + /// this parameter is required. It is not valid with other grant types. + /// + [DataMember(Name = "access_token")] + string AccessToken { get; set; } + + /// + /// The type of grant. Supported grant types are: access_token,password. + /// + [DataMember(Name = "grant_type")] + GrantType? GrantType { get; set; } + + /// + /// The user’s password. If you specify the password grant type, + /// this parameter is required. It is not valid with other grant types. + /// + [DataMember(Name = "password")] + string Password { get; set; } + + /// + /// The user name that identifies the user. If you specify the password grant type, + /// this parameter is required. It is not valid with other grant types. + /// + [DataMember(Name = "username")] + string Username { get; set; } + + /// + /// Defines the API key. + /// + [DataMember(Name = "api_key")] + IApiKey ApiKey { get; set; } + } + + public partial class GrantApiKeyRequest + { + /// + public string AccessToken { get; set; } + + /// + public GrantType? GrantType { get; set; } + + /// + public string Password { get; set; } + + /// + public string Username { get; set; } + + /// + public IApiKey ApiKey { get; set; } + } + + public partial class GrantApiKeyDescriptor + { + /// + string IGrantApiKeyRequest.AccessToken { get; set; } + + /// + GrantType? IGrantApiKeyRequest.GrantType { get; set; } = Nest.GrantType.AccessToken; + + /// + string IGrantApiKeyRequest.Password { get; set; } + + /// + string IGrantApiKeyRequest.Username { get; set; } + + /// + IApiKey IGrantApiKeyRequest.ApiKey { get; set; } + + /// + public GrantApiKeyDescriptor AccessToken(string accessToken) => Assign(accessToken, (a, v) => a.AccessToken = v); + + /// + public GrantApiKeyDescriptor GrantType(GrantType? type) => Assign(type, (a, v) => a.GrantType = v); + + /// + public GrantApiKeyDescriptor Password(string password) => Assign(password, (a, v) => a.Password = v); + + /// + public GrantApiKeyDescriptor Username(string username) => Assign(username, (a, v) => a.Username = v); + + /// + public GrantApiKeyDescriptor ApiKey(Func selector) => + Assign(selector, (a, v) => a.ApiKey = v?.Invoke(new ApiKeyDescriptor())); + } +} diff --git a/src/Nest/XPack/Security/ApiKey/GrantApiKey/GrantApiKeyResponse.cs b/src/Nest/XPack/Security/ApiKey/GrantApiKey/GrantApiKeyResponse.cs new file mode 100644 index 00000000000..ef2eb22c069 --- /dev/null +++ b/src/Nest/XPack/Security/ApiKey/GrantApiKey/GrantApiKeyResponse.cs @@ -0,0 +1,38 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Runtime.Serialization; +using Elasticsearch.Net.Utf8Json; + +namespace Nest +{ + public class GrantApiKeyResponse : ResponseBase + { + /// + /// Id for the API key + /// + [DataMember(Name = "id")] + public string Id { get; internal set; } + + /// + /// Name of the API key + /// + [DataMember(Name = "name")] + public string Name { get; internal set; } + + /// + /// Optional expiration time for the API key in milliseconds + /// + [DataMember(Name = "expiration")] + [JsonFormatter(typeof(NullableDateTimeOffsetEpochMillisecondsFormatter))] + public DateTimeOffset? Expiration { get; internal set; } + + /// + /// Generated API key + /// + [DataMember(Name = "api_key")] + public string ApiKey { get; internal set; } + } +} diff --git a/src/Nest/XPack/Security/ApiKey/GrantApiKey/GrantType.cs b/src/Nest/XPack/Security/ApiKey/GrantApiKey/GrantType.cs new file mode 100644 index 00000000000..0219648a5a3 --- /dev/null +++ b/src/Nest/XPack/Security/ApiKey/GrantApiKey/GrantType.cs @@ -0,0 +1,16 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Runtime.Serialization; +using Elasticsearch.Net; + +namespace Nest +{ + [StringEnum] + public enum GrantType + { + [EnumMember(Value = "password")] Password, + [EnumMember(Value = "access_token")] AccessToken + } +} diff --git a/src/Nest/_Generated/ApiUrlsLookup.generated.cs b/src/Nest/_Generated/ApiUrlsLookup.generated.cs index ab332e0cdec..8f6fa23fb35 100644 --- a/src/Nest/_Generated/ApiUrlsLookup.generated.cs +++ b/src/Nest/_Generated/ApiUrlsLookup.generated.cs @@ -259,6 +259,7 @@ internal static class ApiUrlsLookups internal static ApiUrls SecurityGetUserAccessToken = new ApiUrls(new[]{"_security/oauth2/token"}); internal static ApiUrls SecurityGetUser = new ApiUrls(new[]{"_security/user/{username}", "_security/user"}); internal static ApiUrls SecurityGetUserPrivileges = new ApiUrls(new[]{"_security/user/_privileges"}); + internal static ApiUrls SecurityGrantApiKey = new ApiUrls(new[]{"_security/api_key/grant"}); internal static ApiUrls SecurityHasPrivileges = new ApiUrls(new[]{"_security/user/_has_privileges", "_security/user/{user}/_has_privileges"}); internal static ApiUrls SecurityInvalidateApiKey = new ApiUrls(new[]{"_security/api_key"}); internal static ApiUrls SecurityInvalidateUserAccessToken = new ApiUrls(new[]{"_security/oauth2/token"}); diff --git a/tests/Tests/XPack/Security/ApiKey/GrantApiKey/GrantApiKeyTests.cs b/tests/Tests/XPack/Security/ApiKey/GrantApiKey/GrantApiKeyTests.cs new file mode 100644 index 00000000000..463f977d72b --- /dev/null +++ b/tests/Tests/XPack/Security/ApiKey/GrantApiKey/GrantApiKeyTests.cs @@ -0,0 +1,195 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Threading.Tasks; +using Elastic.Elasticsearch.Xunit.XunitPlumbing; +using Elasticsearch.Net; +using FluentAssertions; +using Nest; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Framework.EndpointTests; +using Tests.Framework.EndpointTests.TestState; + +namespace Tests.XPack.Security.ApiKey.GrantApiKey +{ + [SkipVersion("<7.10.0", "APIs introduced in 7.10.0")] + public class GrantApiKeyTests : CoordinatedIntegrationTestBase + { + private const string PutAdminUserStep = nameof(PutAdminUserStep); + private const string PutTargetUserStep = nameof(PutTargetUserStep); + private const string GrantApiKeyStep = nameof(GrantApiKeyStep); + private const string GrantApiKeyWithExpirationStep = nameof(GrantApiKeyWithExpirationStep); + private const string GenerateAccessTokenStep = nameof(GenerateAccessTokenStep); + private const string GrantApiKeyUsingAccessTokenStep = nameof(GrantApiKeyUsingAccessTokenStep); + + public GrantApiKeyTests(XPackCluster cluster, EndpointUsage usage) : base(new CoordinatedUsage(cluster, usage) + { + { + PutAdminUserStep, u => + u.Calls( + v => new PutUserRequest($"user-{v}-admin") + { + Password = "password", + Roles = new[] { "superuser" }, + FullName = "API key superuser" + }, + (v, d) => d + .Password("password") + .Roles("superuser") + .FullName("API key superuser") + , + (v, c, f) => c.Security.PutUser($"user-{v}-admin", f), + (v, c, f) => c.Security.PutUserAsync($"user-{v}-admin", f), + (v, c, r) => c.Security.PutUser(r), + (v, c, r) => c.Security.PutUserAsync(r) + ) + }, + { + PutTargetUserStep, u => + u.Calls( + v => new PutUserRequest($"user-{v}-target") + { + Password = "password", + Roles = new[] { "basic" }, + FullName = "API key target" + }, + (v, d) => d + .Password("password") + .Roles("basic") + .FullName("API key target") + , + (v, c, f) => c.Security.PutUser($"user-{v}-target", f), + (v, c, f) => c.Security.PutUserAsync($"user-{v}-target", f), + (v, c, r) => c.Security.PutUser(r), + (v, c, r) => c.Security.PutUserAsync(r) + ) + }, + { + GrantApiKeyStep, u => + u.Calls( + v => new GrantApiKeyRequest + { + GrantType = GrantType.Password, + Username = $"user-{v}-target", + Password = "password", + ApiKey = new Nest.ApiKey + { + Name = $"api-key-{v}" + }, + RequestConfiguration = new RequestConfiguration + { + BasicAuthenticationCredentials = new BasicAuthenticationCredentials($"user-{v}-admin", "password") + } + }, + (v, d) => d + .Username($"user-{v}-target") + .Password("password") + .ApiKey(k => k.Name($"api-key-{v}")) + .GrantType(GrantType.Password) + .RequestConfiguration(r => r.BasicAuthentication($"user-{v}-admin", "password")), + (v, c, f) => c.Security.GrantApiKey(f), + (v, c, f) => c.Security.GrantApiKeyAsync(f), + (v, c, r) => c.Security.GrantApiKey(r), + (v, c, r) => c.Security.GrantApiKeyAsync(r) + ) + }, + { + GrantApiKeyWithExpirationStep, u => + u.Calls( + v => new GrantApiKeyRequest + { + GrantType = GrantType.Password, + Username = $"user-{v}-target", + Password = "password", + ApiKey = new Nest.ApiKey + { + Name = $"api-key-{v}", + Expiration = new Time(TimeSpan.FromMinutes(1)) + }, + RequestConfiguration = new RequestConfiguration + { + BasicAuthenticationCredentials = new BasicAuthenticationCredentials($"user-{v}-admin", "password") + } + }, + (v, d) => d + .Username($"user-{v}-target") + .Password("password") + .ApiKey(k => k + .Name($"api-key-{v}") + .Expiration(new Time(TimeSpan.FromMinutes(1)))) + .GrantType(GrantType.Password) + .RequestConfiguration(r => r.BasicAuthentication($"user-{v}-admin", "password")), + (v, c, f) => c.Security.GrantApiKey(f), + (v, c, f) => c.Security.GrantApiKeyAsync(f), + (v, c, r) => c.Security.GrantApiKey(r), + (v, c, r) => c.Security.GrantApiKeyAsync(r) + ) + }, + { + GenerateAccessTokenStep, u => + u.Calls( + v => new GetUserAccessTokenRequest($"user-{v}-target","password"), + (v, d) => d, + (v, c, f) => c.Security.GetUserAccessToken($"user-{v}-target", "password", f), + (v, c, f) => c.Security.GetUserAccessTokenAsync($"user-{v}-target", "password", f), + (_, c, r) => c.Security.GetUserAccessToken(r), + (_, c, r) => c.Security.GetUserAccessTokenAsync(r), + (r, values) => values.ExtendedValue("accessToken", r.AccessToken) + ) + }, + { + GrantApiKeyUsingAccessTokenStep, u => + u.Calls(v => new GrantApiKeyRequest + { + GrantType = GrantType.AccessToken, + AccessToken = u.Usage.CallUniqueValues.ExtendedValue("accessToken") ?? string.Empty, + ApiKey = new Nest.ApiKey + { + Name = $"api-key-{v}" + }, + RequestConfiguration = new RequestConfiguration + { + BasicAuthenticationCredentials = new BasicAuthenticationCredentials($"user-{v}-admin", "password") + } + }, + (v, d) => d + .GrantType(GrantType.AccessToken) + .AccessToken(u.Usage.CallUniqueValues.ExtendedValue("accessToken") ?? string.Empty) + .ApiKey(k => k.Name($"api-key-{v}")) + .RequestConfiguration(r => r.BasicAuthentication($"user-{v}-admin", "password")), + (_, c, f) => c.Security.GrantApiKey(f), + (_, c, f) => c.Security.GrantApiKeyAsync(f), + (_, c, r) => c.Security.GrantApiKey(r), + (_, c, r) => c.Security.GrantApiKeyAsync(r) + ) + } + }) { } + + [I] public async Task GrantApiKeyResponse() => await Assert(GrantApiKeyStep, r => + { + r.IsValid.Should().BeTrue(); + r.Id.Should().NotBeNullOrEmpty(); + r.Name.Should().NotBeNullOrEmpty(); + r.ApiKey.Should().NotBeNullOrEmpty(); + }); + + [I] public async Task GrantApiKeyWithExpirationResponse() => await Assert(GrantApiKeyWithExpirationStep, r => + { + r.IsValid.Should().BeTrue(); + r.Id.Should().NotBeNullOrEmpty(); + r.Name.Should().NotBeNullOrEmpty(); + r.Expiration.Should().NotBeNull(); + r.ApiKey.Should().NotBeNullOrEmpty(); + }); + + [I] public async Task GrantApiKeyWithAccessTokenResponse() => await Assert(GrantApiKeyUsingAccessTokenStep, r => + { + r.IsValid.Should().BeTrue(); + r.Id.Should().NotBeNullOrEmpty(); + r.Name.Should().NotBeNullOrEmpty(); + r.ApiKey.Should().NotBeNullOrEmpty(); + }); + } +} diff --git a/tests/Tests/XPack/Security/ApiKey/GrantApiKey/GrantApiKeyUrlTests.cs b/tests/Tests/XPack/Security/ApiKey/GrantApiKey/GrantApiKeyUrlTests.cs new file mode 100644 index 00000000000..d9cfb63aeb4 --- /dev/null +++ b/tests/Tests/XPack/Security/ApiKey/GrantApiKey/GrantApiKeyUrlTests.cs @@ -0,0 +1,20 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Threading.Tasks; +using Elastic.Elasticsearch.Xunit.XunitPlumbing; +using Nest; +using Tests.Framework.EndpointTests; + +namespace Tests.XPack.Security.ApiKey.GrantApiKey +{ + public class GrantApiKeyUrlTests : UrlTestsBase + { + [U] public override async Task Urls() => await UrlTester.POST("/_security/api_key/grant") + .Fluent(c => c.Security.GrantApiKey(p => p)) + .Request(c => c.Security.GrantApiKey(new GrantApiKeyRequest())) + .FluentAsync(c => c.Security.GrantApiKeyAsync(p => p)) + .RequestAsync(c => c.Security.GrantApiKeyAsync(new GrantApiKeyRequest())); + } +}