Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
253472a
update acl-token List endpoint
MohamedM216 Mar 2, 2026
0190224
add test for token list filtering
MohamedM216 Mar 2, 2026
4ef049d
Merge branch 'master' of github.com:G-Research/consuldotnet into upda…
MohamedM216 Mar 20, 2026
a819315
Merge branch 'master' of github.com:G-Research/consuldotnet into upda…
MohamedM216 Mar 20, 2026
fa25df3
Merge branch 'master' of github.com:G-Research/consuldotnet into upda…
MohamedM216 Mar 20, 2026
d8a16dd
polishing + update list endpoints, but has RS0017 errors
MohamedM216 Mar 20, 2026
def1237
Merge branch 'master' of github.com:G-Research/consuldotnet into upda…
MohamedM216 Mar 20, 2026
3ae9ff8
fix publicapi.unshipped file population (fix RS0017)
MohamedM216 Mar 20, 2026
ce9d08c
fix stackoverflow in the test + polish the test unit
MohamedM216 Mar 20, 2026
407bd53
keep the old code as it is
MohamedM216 Mar 24, 2026
d19fa35
add a one single sufficient overload and keep token constructors unto…
MohamedM216 Mar 24, 2026
4515e34
remove unneeded code
MohamedM216 Mar 24, 2026
44a10c2
Merge branch 'master' of github.com:G-Research/consuldotnet into upda…
MohamedM216 Mar 24, 2026
03cd119
update unshipped.txt file
MohamedM216 Mar 24, 2026
9171c48
simplify test
MohamedM216 Mar 24, 2026
c936662
cleanup
MohamedM216 Mar 24, 2026
5b047ec
test filter by roleid, authmethod and servicename
MohamedM216 Mar 24, 2026
526fe66
improve tests
MohamedM216 Mar 24, 2026
03b2958
fix tests: authmethod couldn't be created locally, only while login
MohamedM216 Mar 24, 2026
d6b899e
add cutoff version for the test
MohamedM216 Mar 24, 2026
a29eb7a
Merge branch 'master' of github.com:G-Research/consuldotnet into upda…
MohamedM216 Mar 24, 2026
95b65cd
remove unneeded properties
MohamedM216 Mar 26, 2026
44a8759
update version exclusion message in the test
MohamedM216 Mar 26, 2026
9fb4659
update unshipped.txt file
MohamedM216 Mar 26, 2026
3c0f5fe
test filtering by AuthMethod
MohamedM216 Mar 26, 2026
b339918
Merge branch 'master' of github.com:G-Research/consuldotnet into upda…
MohamedM216 Mar 26, 2026
f389c58
Merge branch 'master' of github.com:G-Research/consuldotnet into upda…
MohamedM216 Mar 27, 2026
ed927a8
test: fix filtering by authmethod
MohamedM216 Mar 27, 2026
b229ee3
Merge branch 'master' of github.com:G-Research/consuldotnet into upda…
MohamedM216 Apr 2, 2026
8151478
test filtering by AuthMethod
MohamedM216 Apr 2, 2026
347b9f0
group all the code into a single conditional compilation block
MohamedM216 Apr 2, 2026
8c4f4e0
Merge branch 'master' of github.com:G-Research/consuldotnet into upda…
MohamedM216 Apr 2, 2026
9449c34
Merge branch 'master' of github.com:G-Research/consuldotnet into upda…
MohamedM216 Apr 3, 2026
7c3c29d
remove unnecessary properties
MohamedM216 Apr 3, 2026
c1543e6
Update Consul.Test/TokenTest.cs
MohamedM216 Apr 7, 2026
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
144 changes: 144 additions & 0 deletions Consul.Test/TokenTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@
// -----------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Tokens;
using NuGet.Versioning;
using Xunit;

Expand Down Expand Up @@ -279,6 +284,145 @@ public async Task Token_List()
Assert.True(aclList.Response.Length >= 1);
}

[SkippableFact]
public async Task Token_List_FilterBy()
{
var cutOffVersion = SemanticVersion.Parse("1.14.0");
Skip.If(AgentVersion < cutOffVersion, $"This Consul server version({AgentVersion}) doesn't support `ServiceIdentity`. Requires >= {cutOffVersion}.");
Skip.If(string.IsNullOrEmpty(TestHelper.MasterToken));

#if NET5_0_OR_GREATER
// Create a specific Policy to filter by
var policyEntry = new PolicyEntry
{
Name = KVTest.GenerateTestKeyName(),
Description = "Policy used to test token list filtering",
Rules = "key \"\" { policy = \"read\" }",
};
var policy = await _client.Policy.Create(policyEntry);
Assert.NotNull(policy.Response.Name);

// create a test role to filter by
var roleEntry = new RoleEntry
{
Name = KVTest.GenerateTestKeyName(),
Description = "Test Expanded Role",
Policies = new PolicyLink[] { policy.Response },
};
var role = await _client.Role.Create(roleEntry);
Assert.NotNull(role.Response);

var serviceIdentity = new ServiceIdentity
{
ServiceName = "web"
};

var tokenEntry1 = new TokenEntry
{
Description = "Token Linked to Filter Policy",
Policies = new PolicyLink[] { policy.Response },
Roles = new RoleLink[] { role.Response },
Local = true
};
var token1 = await _client.Token.Create(tokenEntry1);
Assert.NotEmpty(token1.Response.Policies);
Assert.NotEmpty(token1.Response.Roles);

var tokenEntry2 = new TokenEntry
{
Description = "Token NOT Linked to Filter Policy",
Local = true,
ServiceIdentities = new ServiceIdentity[] { serviceIdentity },
};
var token2 = await _client.Token.Create(tokenEntry2);
Assert.NotEmpty(token2.Response.Description);
Assert.Equal(serviceIdentity.ServiceName, token2.Response.ServiceIdentities.First().ServiceName);

string pubKeyPem;
string jwt;
using (var rsa = RSA.Create(2048))
{
pubKeyPem = rsa.ExportSubjectPublicKeyInfoPem();
var signingCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256);
var token = new JwtSecurityToken(
issuer: "consul-login-test-issuer",
audience: "consul-login-test",
claims: new[] { new Claim("sub", "test-user") },
notBefore: DateTime.UtcNow.AddMinutes(-1),
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: signingCredentials
);
jwt = new JwtSecurityTokenHandler().WriteToken(token);
}

var authMethodEntry = new AuthMethodEntry
{
Name = "AuthMethodLoginTest",
Type = "jwt",
Description = "JWT Auth Method for Login Testing",
Config = new Dictionary<string, object>
{
["BoundAudiences"] = new[] { "consul-login-test" },
["BoundIssuer"] = "consul-login-test-issuer",
["JWTValidationPubKeys"] = new[] { pubKeyPem },
["ClaimMappings"] = new Dictionary<string, string> { ["sub"] = "user_name" }
}
};
var authMethod = await _client.AuthMethod.Create(authMethodEntry);
Assert.NotNull(authMethod.Response);

var bindingRule = new ACLBindingRule
{
AuthMethod = authMethodEntry.Name,
BindType = "service",
BindName = "web",
Selector = ""
};
var bindingRuleResponse = await _client.BindingRule.Create(bindingRule);
Assert.NotNull(bindingRuleResponse.Response);

var token3 = await _client.AuthMethod.Login(authMethod.Response.Name, jwt);
Assert.NotEmpty(token3.Response.AccessorID);
Assert.NotEmpty(token3.Response.SecretID);
Assert.Equal(authMethodEntry.Name, token3.Response.AuthMethod);

// List tokens filtering by the specific PolicyID
var filteredList = await _client.Token.List(policy.Response.ID, null, null, null);
Assert.NotEmpty(filteredList.Response);
Assert.Contains(filteredList.Response, t => t.AccessorID == token1.Response.AccessorID);
Assert.DoesNotContain(filteredList.Response, t => t.AccessorID == token2.Response.AccessorID);

// List tokens filtering by the specific RoleID
filteredList = await _client.Token.List(null, role.Response.ID, null, null);
Assert.NotEmpty(filteredList.Response);
Assert.Contains(filteredList.Response, t => t.AccessorID == token1.Response.AccessorID);
Assert.DoesNotContain(filteredList.Response, t => t.AccessorID == token2.Response.AccessorID);

// List tokens filtering by the specific ServiceName
filteredList = await _client.Token.List(null, null, serviceIdentity.ServiceName, null);
Assert.NotEmpty(filteredList.Response);
Assert.Contains(filteredList.Response, t => t.AccessorID == token2.Response.AccessorID);
Assert.DoesNotContain(filteredList.Response, t => t.AccessorID == token1.Response.AccessorID);

// List tokens filtering by the specific AuthMethod
filteredList = await _client.Token.List(null, null, null, authMethodEntry.Name);
Assert.NotEmpty(filteredList.Response);
Assert.Contains(filteredList.Response, t => t.AccessorID == token3.Response.AccessorID);
Assert.DoesNotContain(filteredList.Response, t => t.AccessorID == token1.Response.AccessorID);
Assert.DoesNotContain(filteredList.Response, t => t.AccessorID == token2.Response.AccessorID);

// Cleanup
await _client.Token.Delete(token1.Response.AccessorID);
await _client.Token.Delete(token2.Response.AccessorID);
await _client.Policy.Delete(policy.Response.ID);
await _client.Role.Delete(role.Response.ID);
await _client.AuthMethod.Delete(authMethod.Response.Name);
#else
Skip.If(true, "RSA.ExportSubjectPublicKeyInfoPem() is not available before NET5.0");
await Task.CompletedTask;
#endif
}

[SkippableFact]
public async Task Token_ReadSelfToken()
{
Expand Down
4 changes: 3 additions & 1 deletion Consul/Interfaces/ITokenEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ public interface ITokenEndpoint
Task<QueryResult<TokenEntry>> Read(string id, bool expanded, QueryOptions q, CancellationToken ct = default);
Task<QueryResult<TokenEntry[]>> List(CancellationToken ct);
Task<QueryResult<TokenEntry[]>> List();
Task<QueryResult<TokenEntry[]>> List(QueryOptions q, CancellationToken ct = default);
Task<QueryResult<TokenEntry[]>> List(QueryOptions q, CancellationToken ct);
Task<QueryResult<TokenEntry[]>> List(QueryOptions q);
Task<QueryResult<TokenEntry[]>> List(string policy, string role, string serviceName, string authMethod, QueryOptions q = default, CancellationToken ct = default);
Task<WriteResult<TokenEntry>> Update(TokenEntry token, CancellationToken ct);
Task<WriteResult<TokenEntry>> Update(TokenEntry token);
Task<WriteResult<TokenEntry>> Update(TokenEntry token, WriteOptions q, CancellationToken ct = default);
Expand Down
10 changes: 9 additions & 1 deletion Consul/PublicAPI/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ Consul.TokenEntry.ExpandedRoles.get -> Consul.ExpandedRole[]
Consul.TokenEntry.ExpandedRoles.set -> void
Consul.TokenEntry.ResolvedByAgent.get -> string
Consul.TokenEntry.ResolvedByAgent.set -> void
Consul.ITokenEndpoint.List(Consul.QueryOptions q) -> System.Threading.Tasks.Task<Consul.QueryResult<Consul.TokenEntry[]>>
Consul.ITokenEndpoint.List(Consul.QueryOptions q, System.Threading.CancellationToken ct) -> System.Threading.Tasks.Task<Consul.QueryResult<Consul.TokenEntry[]>>
Consul.ITokenEndpoint.List(string policy, string role, string serviceName, string authMethod, Consul.QueryOptions q = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Consul.QueryResult<Consul.TokenEntry[]>>
Consul.Token.List(Consul.QueryOptions q) -> System.Threading.Tasks.Task<Consul.QueryResult<Consul.TokenEntry[]>>
Consul.Token.List(Consul.QueryOptions q, System.Threading.CancellationToken ct) -> System.Threading.Tasks.Task<Consul.QueryResult<Consul.TokenEntry[]>>
Consul.Token.List(string policy, string role, string serviceName, string authMethod, Consul.QueryOptions queryOptions = null, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Consul.QueryResult<Consul.TokenEntry[]>>
*REMOVED*Consul.ITokenEndpoint.Read(string id, Consul.QueryOptions q, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Consul.QueryResult<Consul.TokenEntry>>
*REMOVED*Consul.Token.Read(string id, Consul.QueryOptions queryOptions, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Consul.QueryResult<Consul.TokenEntry>>
*REMOVED*Consul.IAuthMethodEndpoint.Login() -> System.Threading.Tasks.Task<Consul.WriteResult<Consul.TokenEntry>>
Expand All @@ -67,4 +73,6 @@ Consul.TokenEntry.ResolvedByAgent.set -> void
*REMOVED*Consul.AuthMethodEntry.AuthMethodEntry(string name, string type, System.Collections.Generic.Dictionary<string, string> config) -> void
*REMOVED*Consul.AuthMethodEntry.Config.get -> System.Collections.Generic.Dictionary<string, string>
*REMOVED*Consul.AuthMethodEntry.ShouldSerializeCreateIndex() -> bool
*REMOVED*Consul.AuthMethodEntry.ShouldSerializeModifyIndex() -> bool
*REMOVED*Consul.AuthMethodEntry.ShouldSerializeModifyIndex() -> bool
*REMOVED*Consul.ITokenEndpoint.List(Consul.QueryOptions q, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Consul.QueryResult<Consul.TokenEntry[]>>
*REMOVED*Consul.Token.List(Consul.QueryOptions queryOptions, System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Consul.QueryResult<Consul.TokenEntry[]>>
39 changes: 35 additions & 4 deletions Consul/Token.cs
Original file line number Diff line number Diff line change
Expand Up @@ -343,23 +343,54 @@ public async Task<QueryResult<TokenEntry>> Read(string id, bool expanded, QueryO
/// <returns>A query result containing an array of ACL Tokens</returns>
public Task<QueryResult<TokenEntry[]>> List(CancellationToken ct)
{
return List(QueryOptions.Default, ct);
return List(null, null, null, null, QueryOptions.Default, ct);
}

public Task<QueryResult<TokenEntry[]>> List()
{
return List(QueryOptions.Default, CancellationToken.None);
return List(null, null, null, null, QueryOptions.Default, CancellationToken.None);
}

public Task<QueryResult<TokenEntry[]>> List(QueryOptions q, CancellationToken ct)
{
return List(null, null, null, null, q, ct);
}

public Task<QueryResult<TokenEntry[]>> List(QueryOptions q)
{
return List(null, null, null, null, q, CancellationToken.None);
}

/// <summary>
/// Lists the existing ACL Tokens in Consul
/// </summary>
/// <param name="policy">Filters the token list to those tokens that are linked with this specific policy ID</param>
/// <param name="role">Filters the token list to those tokens that are linked with this specific role ID</param>
/// <param name="serviceName">Filters the token list to those tokens that are linked with this specific service name in their service identity</param>
/// <param name="authMethod">Filters the token list to those tokens that are linked with this specific named auth method</param>
/// <param name="queryOptions">Customized query options</param>
/// <param name="ct">Cancellation token for long poll request. If set, OperationCanceledException will be thrown if the request is cancelled before completing</param>
/// <returns>A query result containing an array of ACL Tokens</returns>
public Task<QueryResult<TokenEntry[]>> List(QueryOptions queryOptions, CancellationToken ct = default)
public Task<QueryResult<TokenEntry[]>> List(string policy, string role, string serviceName, string authMethod, QueryOptions queryOptions = default, CancellationToken ct = default)
{
return _client.Get<TokenEntry[]>("/v1/acl/tokens", queryOptions).Execute(ct);
var req = _client.Get<TokenEntry[]>("/v1/acl/tokens", queryOptions);
if (!string.IsNullOrEmpty(policy))
{
req.Params["policy"] = policy;
}
if (!string.IsNullOrEmpty(role))
{
req.Params["role"] = role;
}
if (!string.IsNullOrEmpty(serviceName))
{
req.Params["servicename"] = serviceName;
}
if (!string.IsNullOrEmpty(authMethod))
{
req.Params["authmethod"] = authMethod;
}
return req.Execute(ct);
}

/// <summary>
Expand Down
Loading