diff --git a/src/Keycloak.Net.Core/Models/Groups/Group.cs b/src/Keycloak.Net.Core/Models/Groups/Group.cs index 6eb0b677..528204d8 100644 --- a/src/Keycloak.Net.Core/Models/Groups/Group.cs +++ b/src/Keycloak.Net.Core/Models/Groups/Group.cs @@ -6,8 +6,14 @@ public class Group public string Id { get; set; } [JsonPropertyName("name")] public string Name { get; set; } + [JsonPropertyName("description")] + public string Description { get; set; } [JsonPropertyName("path")] public string Path { get; set; } + [JsonPropertyName("parentId")] + public string ParentId { get; set; } + [JsonPropertyName("subGroupCount")] + public long? SubGroupCount { get; set; } [JsonPropertyName("subGroups")] public IEnumerable Subgroups { get; set; } [JsonPropertyName("realmRoles")] @@ -16,4 +22,6 @@ public class Group public IDictionary> ClientRoles { get; set; } [JsonPropertyName("attributes")] public IDictionary> Attributes { get; set; } -} \ No newline at end of file + [JsonPropertyName("access")] + public IDictionary Access { get; set; } +} diff --git a/src/Keycloak.Net.Core/Models/IdentityProviders/IdentityProvider.cs b/src/Keycloak.Net.Core/Models/IdentityProviders/IdentityProvider.cs index d8161b3b..88964279 100644 --- a/src/Keycloak.Net.Core/Models/IdentityProviders/IdentityProvider.cs +++ b/src/Keycloak.Net.Core/Models/IdentityProviders/IdentityProvider.cs @@ -4,6 +4,8 @@ public class IdentityProvider { [JsonPropertyName("alias")] public string Alias { get; set; } + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } [JsonPropertyName("internalId")] public string InternalId { get; set; } [JsonPropertyName("providerId")] @@ -22,8 +24,18 @@ public class IdentityProvider public bool? AuthenticateByDefault { get; set; } [JsonPropertyName("linkOnly")] public bool? LinkOnly { get; set; } + [JsonPropertyName("hideOnLogin")] + public bool? HideOnLogin { get; set; } [JsonPropertyName("firstBrokerLoginFlowAlias")] public string FirstBrokerLoginFlowAlias { get; set; } + [JsonPropertyName("postBrokerLoginFlowAlias")] + public string? PostBrokerLoginFlowAlias { get; set; } + [JsonPropertyName("organizationId")] + public string? OrganizationId { get; set; } [JsonPropertyName("config")] public Config Config { get; set; } -} \ No newline at end of file + [JsonPropertyName("types")] + public IEnumerable? Types { get; set; } + [JsonPropertyName("updateProfileFirstLogin")] + public bool? UpdateProfileFirstLogin { get; set; } +} diff --git a/src/Keycloak.Net.Core/Models/Organizations/Organization.cs b/src/Keycloak.Net.Core/Models/Organizations/Organization.cs index 4b791010..6b5c01e5 100644 --- a/src/Keycloak.Net.Core/Models/Organizations/Organization.cs +++ b/src/Keycloak.Net.Core/Models/Organizations/Organization.cs @@ -1,4 +1,5 @@ using Keycloak.Net.Core.Models.Organizations; +using Keycloak.Net.Models.Groups; using Keycloak.Net.Models.IdentityProviders; namespace Keycloak.Net.Models.Organizations; @@ -34,4 +35,7 @@ public class Organization [JsonPropertyName("identityProviders")] public IEnumerable? IdentityProviders { get; set; } -} \ No newline at end of file + + [JsonPropertyName("groups")] + public IEnumerable? Groups { get; set; } +} diff --git a/src/Keycloak.Net.Core/Models/Organizations/OrganizationInvitation.cs b/src/Keycloak.Net.Core/Models/Organizations/OrganizationInvitation.cs new file mode 100644 index 00000000..07d5b78b --- /dev/null +++ b/src/Keycloak.Net.Core/Models/Organizations/OrganizationInvitation.cs @@ -0,0 +1,31 @@ +namespace Keycloak.Net.Models.Organizations; + +public class OrganizationInvitation +{ + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("organizationId")] + public string? OrganizationId { get; set; } + + [JsonPropertyName("email")] + public string? Email { get; set; } + + [JsonPropertyName("firstName")] + public string? FirstName { get; set; } + + [JsonPropertyName("lastName")] + public string? LastName { get; set; } + + [JsonPropertyName("sentDate")] + public int? SentDate { get; set; } + + [JsonPropertyName("expiresAt")] + public int? ExpiresAt { get; set; } + + [JsonPropertyName("status")] + public string? Status { get; set; } + + [JsonPropertyName("inviteLink")] + public string? InviteLink { get; set; } +} diff --git a/src/Keycloak.Net.Core/Models/Users/SocialLink.cs b/src/Keycloak.Net.Core/Models/Users/SocialLink.cs new file mode 100644 index 00000000..798300f7 --- /dev/null +++ b/src/Keycloak.Net.Core/Models/Users/SocialLink.cs @@ -0,0 +1,13 @@ +namespace Keycloak.Net.Models.Users; + +public class SocialLink +{ + [JsonPropertyName("socialProvider")] + public string? SocialProvider { get; set; } + + [JsonPropertyName("socialUserId")] + public string? SocialUserId { get; set; } + + [JsonPropertyName("socialUsername")] + public string? SocialUsername { get; set; } +} diff --git a/src/Keycloak.Net.Core/Models/Users/User.cs b/src/Keycloak.Net.Core/Models/Users/User.cs index fd77da6c..2a48253b 100644 --- a/src/Keycloak.Net.Core/Models/Users/User.cs +++ b/src/Keycloak.Net.Core/Models/Users/User.cs @@ -30,6 +30,8 @@ public class User public UserAccess Access { get; set; } [JsonPropertyName("attributes")] public Dictionary> Attributes { get; set; } + [JsonPropertyName("userProfileMetadata")] + public UserProfileMetadata? UserProfileMetadata { get; set; } [JsonPropertyName("clientConsents")] public IEnumerable ClientConsents { get; set; } [JsonPropertyName("clientRoles")] @@ -42,6 +44,10 @@ public class User public string FederationLink { get; set; } [JsonPropertyName("groups")] public IEnumerable Groups { get; set; } + [JsonPropertyName("applicationRoles")] + public IDictionary>? ApplicationRoles { get; set; } + [JsonPropertyName("socialLinks")] + public IEnumerable? SocialLinks { get; set; } [JsonPropertyName("origin")] public string Origin { get; set; } [JsonPropertyName("realmRoles")] @@ -50,4 +56,4 @@ public class User public string Self { get; set; } [JsonPropertyName("serviceAccountClientId")] public string ServiceAccountClientId { get; set; } -} \ No newline at end of file +} diff --git a/src/Keycloak.Net.Core/Models/Users/UserProfileAttributeGroupMetadata.cs b/src/Keycloak.Net.Core/Models/Users/UserProfileAttributeGroupMetadata.cs new file mode 100644 index 00000000..183d15a2 --- /dev/null +++ b/src/Keycloak.Net.Core/Models/Users/UserProfileAttributeGroupMetadata.cs @@ -0,0 +1,16 @@ +namespace Keycloak.Net.Models.Users; + +public class UserProfileAttributeGroupMetadata +{ + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("displayHeader")] + public string? DisplayHeader { get; set; } + + [JsonPropertyName("displayDescription")] + public string? DisplayDescription { get; set; } + + [JsonPropertyName("annotations")] + public IDictionary? Annotations { get; set; } +} diff --git a/src/Keycloak.Net.Core/Models/Users/UserProfileAttributeMetadata.cs b/src/Keycloak.Net.Core/Models/Users/UserProfileAttributeMetadata.cs new file mode 100644 index 00000000..b647ca29 --- /dev/null +++ b/src/Keycloak.Net.Core/Models/Users/UserProfileAttributeMetadata.cs @@ -0,0 +1,31 @@ +namespace Keycloak.Net.Models.Users; + +public class UserProfileAttributeMetadata +{ + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + [JsonPropertyName("required")] + public bool? Required { get; set; } + + [JsonPropertyName("readOnly")] + public bool? ReadOnly { get; set; } + + [JsonPropertyName("annotations")] + public IDictionary? Annotations { get; set; } + + [JsonPropertyName("validators")] + public IDictionary>? Validators { get; set; } + + [JsonPropertyName("group")] + public string? Group { get; set; } + + [JsonPropertyName("multivalued")] + public bool? Multivalued { get; set; } + + [JsonPropertyName("defaultValue")] + public string? DefaultValue { get; set; } +} diff --git a/src/Keycloak.Net.Core/Models/Users/UserProfileMetadata.cs b/src/Keycloak.Net.Core/Models/Users/UserProfileMetadata.cs new file mode 100644 index 00000000..bee472f5 --- /dev/null +++ b/src/Keycloak.Net.Core/Models/Users/UserProfileMetadata.cs @@ -0,0 +1,10 @@ +namespace Keycloak.Net.Models.Users; + +public class UserProfileMetadata +{ + [JsonPropertyName("attributes")] + public IEnumerable? Attributes { get; set; } + + [JsonPropertyName("groups")] + public IEnumerable? Groups { get; set; } +} diff --git a/src/Keycloak.Net.Core/Organizations/KeycloakClient.cs b/src/Keycloak.Net.Core/Organizations/KeycloakClient.cs index 0efd4120..6758fccc 100644 --- a/src/Keycloak.Net.Core/Organizations/KeycloakClient.cs +++ b/src/Keycloak.Net.Core/Organizations/KeycloakClient.cs @@ -1,5 +1,7 @@ using System; using System.Linq; +using System.Net.Http; +using Keycloak.Net.Models.Groups; using Keycloak.Net.Models.IdentityProviders; using Keycloak.Net.Models.Organizations; @@ -77,6 +79,206 @@ private static string GetResourceIdentifierFromLocation(Uri? location) return location?.Segments.LastOrDefault() ?? throw new InvalidOperationException($"\"{nameof(location)}\" is invalid."); } + public async Task GetOrganizationsCountAsync(string realm, + bool? exact = null, + string? q = null, + string? search = null, + CancellationToken cancellationToken = default) + { + var queryParams = new Dictionary + { + [nameof(exact)] = exact, + [nameof(q)] = q, + [nameof(search)] = search + }; + + return await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/count") + .SetQueryParams(queryParams) + .GetJsonAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + public async Task> GetOrganizationGroupsAsync(string realm, + string organizationId, + bool? briefRepresentation = null, + bool? exact = null, + int? first = null, + int? max = null, + bool? populateHierarchy = null, + string? q = null, + string? search = null, + bool? subGroupsCount = null, + CancellationToken cancellationToken = default) + { + var queryParams = new Dictionary + { + [nameof(briefRepresentation)] = briefRepresentation, + [nameof(exact)] = exact, + [nameof(first)] = first, + [nameof(max)] = max, + [nameof(populateHierarchy)] = populateHierarchy, + [nameof(q)] = q, + [nameof(search)] = search, + [nameof(subGroupsCount)] = subGroupsCount + }; + + return await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/groups") + .SetQueryParams(queryParams) + .GetJsonAsync>(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + public async Task CreateOrganizationGroupAsync(string realm, + string organizationId, + Group group, + CancellationToken cancellationToken = default) + { + var response = await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/groups") + .PostJsonAsync(group, cancellationToken: cancellationToken) + .ConfigureAwait(false); + return response.ResponseMessage.IsSuccessStatusCode; + } + + public async Task GetOrganizationGroupByPathAsync(string realm, + string organizationId, + string path, + bool? subGroupsCount = null, + CancellationToken cancellationToken = default) + { + var queryParams = new Dictionary + { + [nameof(subGroupsCount)] = subGroupsCount + }; + + return await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/groups/group-by-path") + .AppendPathSegment(path, true) + .SetQueryParams(queryParams) + .GetJsonAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + public async Task GetOrganizationGroupAsync(string realm, + string organizationId, + string groupId, + bool? subGroupsCount = null, + CancellationToken cancellationToken = default) + { + var queryParams = new Dictionary + { + [nameof(subGroupsCount)] = subGroupsCount + }; + + return await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/groups/{groupId}") + .SetQueryParams(queryParams) + .GetJsonAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + public async Task UpdateOrganizationGroupAsync(string realm, + string organizationId, + string groupId, + Group group, + CancellationToken cancellationToken = default) + { + var response = await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/groups/{groupId}") + .PutJsonAsync(group, cancellationToken: cancellationToken) + .ConfigureAwait(false); + return response.ResponseMessage.IsSuccessStatusCode; + } + + public async Task DeleteOrganizationGroupAsync(string realm, + string organizationId, + string groupId, + CancellationToken cancellationToken = default) + { + var response = await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/groups/{groupId}") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + return response.ResponseMessage.IsSuccessStatusCode; + } + + public async Task> GetOrganizationGroupChildrenAsync(string realm, + string organizationId, + string groupId, + bool? exact = null, + int? first = null, + int? max = null, + string? search = null, + bool? subGroupsCount = null, + CancellationToken cancellationToken = default) + { + var queryParams = new Dictionary + { + [nameof(exact)] = exact, + [nameof(first)] = first, + [nameof(max)] = max, + [nameof(search)] = search, + [nameof(subGroupsCount)] = subGroupsCount + }; + + return await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/groups/{groupId}/children") + .SetQueryParams(queryParams) + .GetJsonAsync>(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + public async Task SetOrCreateOrganizationGroupChildAsync(string realm, + string organizationId, + string groupId, + Group group, + CancellationToken cancellationToken = default) + { + var response = await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/groups/{groupId}/children") + .PostJsonAsync(group, cancellationToken: cancellationToken) + .ConfigureAwait(false); + return response.ResponseMessage.IsSuccessStatusCode; + } + + public async Task> GetOrganizationGroupMembersAsync(string realm, + string organizationId, + string groupId, + bool? briefRepresentation = null, + int? first = null, + int? max = null, + CancellationToken cancellationToken = default) + { + var queryParams = new Dictionary + { + [nameof(briefRepresentation)] = briefRepresentation, + [nameof(first)] = first, + [nameof(max)] = max + }; + + return await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/groups/{groupId}/members") + .SetQueryParams(queryParams) + .GetJsonAsync>(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + public async Task AddOrganizationGroupMemberAsync(string realm, + string organizationId, + string groupId, + string userId, + CancellationToken cancellationToken = default) + { + var response = await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/groups/{groupId}/members/{userId}") + .PutAsync(new StringContent(""), cancellationToken: cancellationToken) + .ConfigureAwait(false); + return response.ResponseMessage.IsSuccessStatusCode; + } + + public async Task DeleteOrganizationGroupMemberAsync(string realm, + string organizationId, + string groupId, + string userId, + CancellationToken cancellationToken = default) + { + var response = await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/groups/{groupId}/members/{userId}") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + return response.ResponseMessage.IsSuccessStatusCode; + } + public async Task DeleteOrganizationIdentityProviderAsync(string realm, string organizationId, string identityProviderAlias, @@ -114,6 +316,93 @@ public async Task AddOrganizationIdentityProviderAsync(string realm, return response.ResponseMessage.IsSuccessStatusCode; } + public async Task> GetOrganizationIdentityProviderGroupsAsync(string realm, + string organizationId, + string identityProviderAlias, + bool? briefRepresentation = null, + bool? exact = null, + int? first = null, + int? max = null, + string? q = null, + string? search = null, + bool? subGroupsCount = null, + CancellationToken cancellationToken = default) + { + var queryParams = new Dictionary + { + [nameof(briefRepresentation)] = briefRepresentation, + [nameof(exact)] = exact, + [nameof(first)] = first, + [nameof(max)] = max, + [nameof(q)] = q, + [nameof(search)] = search, + [nameof(subGroupsCount)] = subGroupsCount + }; + + return await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/identity-providers/{identityProviderAlias}/groups") + .SetQueryParams(queryParams) + .GetJsonAsync>(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + public async Task> GetOrganizationInvitationsAsync(string realm, + string organizationId, + string? email = null, + int? first = null, + string? firstName = null, + string? lastName = null, + int? max = null, + string? search = null, + string? status = null, + CancellationToken cancellationToken = default) + { + var queryParams = new Dictionary + { + [nameof(email)] = email, + [nameof(first)] = first, + [nameof(firstName)] = firstName, + [nameof(lastName)] = lastName, + [nameof(max)] = max, + [nameof(search)] = search, + [nameof(status)] = status + }; + + return await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/invitations") + .SetQueryParams(queryParams) + .GetJsonAsync>(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + public async Task GetOrganizationInvitationAsync(string realm, + string organizationId, + string invitationId, + CancellationToken cancellationToken = default) => + await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/invitations/{invitationId}") + .GetJsonAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + public async Task DeleteOrganizationInvitationAsync(string realm, + string organizationId, + string invitationId, + CancellationToken cancellationToken = default) + { + var response = await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/invitations/{invitationId}") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + return response.ResponseMessage.IsSuccessStatusCode; + } + + public async Task ResendOrganizationInvitationAsync(string realm, + string organizationId, + string invitationId, + CancellationToken cancellationToken = default) + { + var response = await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/invitations/{invitationId}/resend") + .PostAsync(new StringContent(""), cancellationToken: cancellationToken) + .ConfigureAwait(false); + return response.ResponseMessage.IsSuccessStatusCode; + } + public async Task GetOrganizationMembersCountAsync(string realm, string organizationId, CancellationToken cancellationToken = default) => @@ -127,6 +416,7 @@ public async Task> GetOrganizationMembersAsync(string realm, int? first = null, int? max = null, string? search = null, + string? membershipType = null, CancellationToken cancellationToken = default) { var queryParams = new Dictionary @@ -134,6 +424,7 @@ public async Task> GetOrganizationMembersAsync(string realm, [nameof(exact)] = exact, [nameof(first)] = first, [nameof(max)] = max, + [nameof(membershipType)] = membershipType, [nameof(search)] = search }; var response = await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/members") @@ -167,13 +458,43 @@ public async Task DeleteMemberFromOrganizationAsync(string realm, public async Task> GetOrganizationsForMemberAsync(string realm, string organizationId, string memberId, + bool? briefRepresentation = null, CancellationToken cancellationToken = default) { + var queryParams = new Dictionary + { + [nameof(briefRepresentation)] = briefRepresentation, + }; + return await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/members/{memberId}/organizations") + .SetQueryParams(queryParams) .GetJsonAsync>(cancellationToken: cancellationToken) .ConfigureAwait(false); } + public async Task> GetOrganizationMemberGroupsAsync(string realm, + string organizationId, + string memberId, + bool? briefRepresentation = null, + int? first = null, + int? max = null, + string? search = null, + CancellationToken cancellationToken = default) + { + var queryParams = new Dictionary + { + [nameof(briefRepresentation)] = briefRepresentation, + [nameof(first)] = first, + [nameof(max)] = max, + [nameof(search)] = search + }; + + return await GetBaseUrl(realm).AppendPathSegment($"/admin/realms/{realm}/organizations/{organizationId}/members/{memberId}/groups") + .SetQueryParams(queryParams) + .GetJsonAsync>(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + public async Task> GetOrganizationsForMemberAsync(string realm, string memberId, bool? briefRepresentation = null,