Skip to content

Commit bd4dc32

Browse files
author
Ghaith Prosoft
committed
Add support for managing organizations in Keycloak
Introduced a new `IKcOrganizations` interface and its implementation in `KcOrganizations` to enable CRUD operations, listing, and filtering of organizations in Keycloak. Updated the `IKeycloakClient` and `KeycloakClient` classes to expose the `Organizations` client. Added supporting models: - `KcOrganization` to represent organization resources. - `KcOrganizationDomain` to represent domain information. - `KcOrganizationFilter` to enable filtering options for queries. Updated `NETCore.Keycloak.Client.csproj` to clean up formatting and remove unnecessary metadata files.
1 parent 57d1553 commit bd4dc32

8 files changed

Lines changed: 458 additions & 12 deletions

File tree

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
using NETCore.Keycloak.Client.Models;
2+
using NETCore.Keycloak.Client.Models.Organizations;
3+
4+
namespace NETCore.Keycloak.Client.HttpClients.Abstraction;
5+
6+
/// <summary>
7+
/// Keycloak organizations REST client
8+
/// </summary>
9+
public interface IKcOrganizations
10+
{
11+
/// <summary>
12+
/// Creates a new organization in a specified Keycloak realm.
13+
///
14+
/// POST /{realm}/organizations
15+
/// </summary>
16+
/// <param name="realm">The Keycloak realm where the organization will be created.</param>
17+
/// <param name="accessToken">The access token used for authentication.</param>
18+
/// <param name="organization">The organization representation to create.</param>
19+
/// <param name="cancellationToken">Optional cancellation token.</param>
20+
/// <returns>A <see cref="KcResponse{T}"/> indicating the result.</returns>
21+
Task<KcResponse<object>> CreateAsync(
22+
string realm,
23+
string accessToken,
24+
KcOrganization organization,
25+
CancellationToken cancellationToken = default);
26+
27+
/// <summary>
28+
/// Updates an existing organization in a specified Keycloak realm.
29+
///
30+
/// PUT /{realm}/organizations/{organizationId}
31+
/// </summary>
32+
/// <param name="realm">The Keycloak realm where the organization exists.</param>
33+
/// <param name="accessToken">The access token used for authentication.</param>
34+
/// <param name="organizationId">The ID of the organization to update.</param>
35+
/// <param name="organization">The updated organization representation.</param>
36+
/// <param name="cancellationToken">Optional cancellation token.</param>
37+
/// <returns>A <see cref="KcResponse{T}"/> indicating the result.</returns>
38+
Task<KcResponse<object>> UpdateAsync(
39+
string realm,
40+
string accessToken,
41+
string organizationId,
42+
KcOrganization organization,
43+
CancellationToken cancellationToken = default);
44+
45+
/// <summary>
46+
/// Deletes an organization from a specified Keycloak realm.
47+
///
48+
/// DELETE /{realm}/organizations/{organizationId}
49+
/// </summary>
50+
/// <param name="realm">The Keycloak realm where the organization exists.</param>
51+
/// <param name="accessToken">The access token used for authentication.</param>
52+
/// <param name="organizationId">The ID of the organization to delete.</param>
53+
/// <param name="cancellationToken">Optional cancellation token.</param>
54+
/// <returns>A <see cref="KcResponse{T}"/> indicating the result.</returns>
55+
Task<KcResponse<object>> DeleteAsync(
56+
string realm,
57+
string accessToken,
58+
string organizationId,
59+
CancellationToken cancellationToken = default);
60+
61+
/// <summary>
62+
/// Retrieves a specific organization by its ID from a specified Keycloak realm.
63+
///
64+
/// GET /{realm}/organizations/{organizationId}
65+
/// </summary>
66+
/// <param name="realm">The Keycloak realm to query.</param>
67+
/// <param name="accessToken">The access token used for authentication.</param>
68+
/// <param name="organizationId">The ID of the organization to retrieve.</param>
69+
/// <param name="cancellationToken">Optional cancellation token.</param>
70+
/// <returns>A <see cref="KcResponse{OrganizationRepresentation}"/> with the organization details.</returns>
71+
Task<KcResponse<KcOrganization>> GetAsync(
72+
string realm,
73+
string accessToken,
74+
string organizationId,
75+
CancellationToken cancellationToken = default);
76+
77+
/// <summary>
78+
/// Retrieves a list of organizations from a specified Keycloak realm, optionally filtered by criteria.
79+
///
80+
/// GET /{realm}/organizations
81+
/// </summary>
82+
/// <param name="realm">The Keycloak realm from which organizations will be listed.</param>
83+
/// <param name="accessToken">The access token used for authentication.</param>
84+
/// <param name="filter">Optional filter criteria.</param>
85+
/// <param name="cancellationToken">Optional cancellation token.</param>
86+
/// <returns>A <see cref="KcResponse{T}"/> containing an enumerable of organizations.</returns>
87+
Task<KcResponse<IEnumerable<KcOrganization>>> ListAsync(
88+
string realm,
89+
string accessToken,
90+
KcOrganizationFilter filter = null,
91+
CancellationToken cancellationToken = default);
92+
93+
/// <summary>
94+
/// Retrieves the count of organizations in a specified Keycloak realm, optionally filtered.
95+
///
96+
/// GET /{realm}/organizations/count
97+
/// </summary>
98+
/// <param name="realm">The Keycloak realm to query.</param>
99+
/// <param name="accessToken">The access token used for authentication.</param>
100+
/// <param name="filter">Optional filter criteria.</param>
101+
/// <param name="cancellationToken">Optional cancellation token.</param>
102+
/// <returns>A <see cref="KcResponse{long}"/> with the count of organizations.</returns>
103+
Task<KcResponse<long>> CountAsync(
104+
string realm,
105+
string accessToken,
106+
KcOrganizationFilter filter = null,
107+
CancellationToken cancellationToken = default);
108+
}

NETCore.Keycloak.Client/HttpClients/Abstraction/IKeycloakClient.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,9 @@ public interface IKeycloakClient
7676
/// See <see cref="IKcScopeMappings"/> for detailed operations.
7777
/// </summary>
7878
public IKcScopeMappings ScopeMappings { get; }
79+
80+
/// <summary>
81+
/// Gets the organizations REST client for managing organizations.
82+
/// </summary>
83+
public IKcOrganizations Organizations { get; }
7984
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
using Microsoft.Extensions.Logging;
2+
using NETCore.Keycloak.Client.HttpClients.Abstraction;
3+
using NETCore.Keycloak.Client.Models;
4+
using NETCore.Keycloak.Client.Models.Organizations;
5+
6+
namespace NETCore.Keycloak.Client.HttpClients.Implementation;
7+
8+
/// <summary>
9+
/// Organization service API for managing organizations in Keycloak.
10+
/// </summary>
11+
internal sealed class KcOrganizations(string baseUrl, ILogger logger) : KcHttpClientBase(logger, baseUrl), IKcOrganizations
12+
{
13+
// Primary constructor on the class declaration is used; no explicit ctor body required.
14+
15+
public Task<KcResponse<object>> CreateAsync(
16+
string realm,
17+
string accessToken,
18+
KcOrganization organization,
19+
CancellationToken cancellationToken = default)
20+
{
21+
ValidateAccess(realm, accessToken);
22+
ValidateNotNull(nameof(organization), organization);
23+
24+
var url = $"{BaseUrl}/{realm}/organizations";
25+
return ProcessRequestAsync<object>(
26+
url,
27+
HttpMethod.Post,
28+
accessToken,
29+
"Unable to create organization",
30+
organization,
31+
"application/json",
32+
cancellationToken);
33+
}
34+
35+
public Task<KcResponse<object>> UpdateAsync(
36+
string realm,
37+
string accessToken,
38+
string organizationId,
39+
KcOrganization organization,
40+
CancellationToken cancellationToken = default)
41+
{
42+
ValidateAccess(realm, accessToken);
43+
ValidateRequiredString(nameof(organizationId), organizationId);
44+
ValidateNotNull(nameof(organization), organization);
45+
46+
var url = $"{BaseUrl}/{realm}/organizations/{organizationId}";
47+
return ProcessRequestAsync<object>(
48+
url,
49+
HttpMethod.Put,
50+
accessToken,
51+
"Unable to update organization",
52+
organization,
53+
"application/json",
54+
cancellationToken);
55+
}
56+
57+
public Task<KcResponse<object>> DeleteAsync(
58+
string realm,
59+
string accessToken,
60+
string organizationId,
61+
CancellationToken cancellationToken = default)
62+
{
63+
ValidateAccess(realm, accessToken);
64+
ValidateRequiredString(nameof(organizationId), organizationId);
65+
66+
var url = $"{BaseUrl}/{realm}/organizations/{organizationId}";
67+
return ProcessRequestAsync<object>(
68+
url,
69+
HttpMethod.Delete,
70+
accessToken,
71+
"Unable to delete organization",
72+
null,
73+
"application/json",
74+
cancellationToken);
75+
}
76+
77+
public Task<KcResponse<KcOrganization>> GetAsync(
78+
string realm,
79+
string accessToken,
80+
string organizationId,
81+
CancellationToken cancellationToken = default)
82+
{
83+
ValidateAccess(realm, accessToken);
84+
ValidateRequiredString(nameof(organizationId), organizationId);
85+
86+
var url = $"{BaseUrl}/{realm}/organizations/{organizationId}";
87+
return ProcessRequestAsync<KcOrganization>(
88+
url,
89+
HttpMethod.Get,
90+
accessToken,
91+
"Unable to get organization",
92+
null,
93+
"application/json",
94+
cancellationToken);
95+
}
96+
97+
public Task<KcResponse<IEnumerable<KcOrganization>>> ListAsync(
98+
string realm,
99+
string accessToken,
100+
KcOrganizationFilter filter = null,
101+
CancellationToken cancellationToken = default)
102+
{
103+
ValidateAccess(realm, accessToken);
104+
filter ??= new KcOrganizationFilter();
105+
106+
var url = $"{BaseUrl}/{realm}/organizations{filter.BuildQuery()}";
107+
return ProcessRequestAsync<IEnumerable<KcOrganization>>(
108+
url,
109+
HttpMethod.Get,
110+
accessToken,
111+
"Unable to list organizations",
112+
null,
113+
"application/json",
114+
cancellationToken);
115+
}
116+
117+
public Task<KcResponse<long>> CountAsync(
118+
string realm,
119+
string accessToken,
120+
KcOrganizationFilter filter = null,
121+
CancellationToken cancellationToken = default)
122+
{
123+
ValidateAccess(realm, accessToken);
124+
filter ??= new KcOrganizationFilter();
125+
126+
var url = $"{BaseUrl}/{realm}/organizations/count{filter.BuildQuery()}";
127+
return ProcessRequestAsync<long>(
128+
url,
129+
HttpMethod.Get,
130+
accessToken,
131+
"Unable to count organizations",
132+
null,
133+
"application/json",
134+
cancellationToken);
135+
}
136+
}

NETCore.Keycloak.Client/HttpClients/Implementation/KeycloakClient.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ public sealed class KeycloakClient : IKeycloakClient
4444
/// <inheritdoc cref="IKeycloakClient.ScopeMappings"/>
4545
public IKcScopeMappings ScopeMappings { get; }
4646

47+
/// <inheritdoc cref="IKeycloakClient.Organizations"/>
48+
public IKcOrganizations Organizations { get; }
49+
4750
/// <summary>
4851
/// Initializes a new instance of the <see cref="KeycloakClient"/> class.
4952
/// Provides access to various Keycloak API services through respective clients.
@@ -86,5 +89,6 @@ public KeycloakClient(string baseUrl, ILogger logger = null)
8689
ProtocolMappers = new KcProtocolMappers(adminUrl, logger);
8790
ScopeMappings = new KcScopeMappings(adminUrl, logger);
8891
RoleMappings = new KcRoleMappings(adminUrl, logger);
92+
Organizations = new KcOrganizations(adminUrl, logger);
8993
}
9094
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace NETCore.Keycloak.Client.Models.Organizations;
4+
5+
/// <summary>
6+
/// Represents an organization resource.
7+
/// </summary>
8+
public sealed class KcOrganization
9+
{
10+
/// <summary>
11+
/// Gets or sets the organization id.
12+
/// </summary>
13+
[JsonPropertyName("id")]
14+
public string Id { get; set; }
15+
16+
/// <summary>
17+
/// Gets or sets the organization name.
18+
/// </summary>
19+
[JsonPropertyName("name")]
20+
public string Name { get; set; }
21+
22+
/// <summary>
23+
/// Gets or sets the organization alias.
24+
/// </summary>
25+
[JsonPropertyName("alias")]
26+
public string Alias { get; set; }
27+
28+
/// <summary>
29+
/// Gets or sets a value indicating whether the organization is enabled.
30+
/// </summary>
31+
[JsonPropertyName("enabled")]
32+
public bool? Enabled { get; set; }
33+
34+
/// <summary>
35+
/// Gets or sets the organization description.
36+
/// </summary>
37+
[JsonPropertyName("description")]
38+
public string Description { get; set; }
39+
40+
/// <summary>
41+
/// Gets or sets the redirect URL for the organization.
42+
/// </summary>
43+
[JsonPropertyName("redirectUrl")]
44+
public string RedirectUrl { get; set; }
45+
46+
/// <summary>
47+
/// Custom attributes.
48+
/// Key = attribute name
49+
/// Value = list of values (Keycloak style)
50+
/// </summary>
51+
[JsonPropertyName("attributes")]
52+
public Dictionary<string, List<string>> Attributes { get; set; }
53+
54+
/// <summary>
55+
/// Gets or sets the organization domains.
56+
/// </summary>
57+
[JsonPropertyName("domains")]
58+
public List<KcOrganizationDomain> Domains { get; set; } = new();
59+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace NETCore.Keycloak.Client.Models.Organizations;
4+
5+
/// <summary>
6+
/// Represents organization domain information.
7+
/// </summary>
8+
public sealed class KcOrganizationDomain
9+
{
10+
/// <summary>
11+
/// Gets or sets the domain name.
12+
/// </summary>
13+
[JsonPropertyName("name")]
14+
public string Name { get; set; }
15+
16+
/// <summary>
17+
/// Gets or sets a value indicating whether the domain is verified.
18+
/// </summary>
19+
[JsonPropertyName("verified")]
20+
public bool? Verified { get; set; }
21+
}

0 commit comments

Comments
 (0)