mirror of
https://github.com/keycloak/keycloak.git
synced 2026-10-23 19:29:23 +03:00
Service account user ID available on client representation instead of exposing separate endpoint returning whole user representation
Closes #54047 Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
@@ -52,6 +52,7 @@ public class ClientRepresentation {
|
||||
protected Boolean implicitFlowEnabled;
|
||||
protected Boolean directAccessGrantsEnabled;
|
||||
protected Boolean serviceAccountsEnabled;
|
||||
protected String serviceAccountUserId;
|
||||
protected Boolean authorizationServicesEnabled;
|
||||
@Deprecated
|
||||
protected Boolean directGrantsOnly;
|
||||
@@ -276,6 +277,14 @@ public class ClientRepresentation {
|
||||
this.serviceAccountsEnabled = serviceAccountsEnabled;
|
||||
}
|
||||
|
||||
public String getServiceAccountUserId() {
|
||||
return serviceAccountUserId;
|
||||
}
|
||||
|
||||
public void setServiceAccountUserId(String serviceAccountUserId) {
|
||||
this.serviceAccountUserId = serviceAccountUserId;
|
||||
}
|
||||
|
||||
public Boolean getAuthorizationServicesEnabled() {
|
||||
if (authorizationSettings != null) {
|
||||
return true;
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
= Administration
|
||||
|
||||
== Service account user ID available on client representation
|
||||
|
||||
The service account user ID is now included directly in the client representation as the `serviceAccountUserId` field.
|
||||
This removes the need for a separate API call to discover the service account user, simplifying client administration and role mapping workflows.
|
||||
The dedicated `GET /admin/realms/\{realm}/clients/\{id}/service-account-user` endpoint has been removed.
|
||||
See the upgrading guide for migration details.
|
||||
|
||||
= Security and Standards
|
||||
|
||||
== Credential ID in credential update events
|
||||
|
||||
@@ -141,6 +141,28 @@ Providers extending `OAuth2IdentityProviderConfig` can instead override `getClie
|
||||
The Client Initiated Backchannel Authentication (CIBA) grant is now rejected for public clients, as required by the https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html#rfc.section.7.1[CIBA specification], which mandates client authentication on both the backchannel authentication endpoint and the token endpoint.
|
||||
Previously, a public client with the CIBA grant enabled could complete the flow without authenticating and obtain tokens for the approving user.
|
||||
|
||||
=== Service account user endpoint removed from Admin REST API
|
||||
// https://github.com/keycloak/keycloak/issues/54047
|
||||
|
||||
The `GET /admin/realms/\{realm}/clients/\{id}/service-account-user` endpoint has been removed.
|
||||
The service account user ID is now available directly on the client representation as the `serviceAccountUserId` field, which is populated when `serviceAccountsEnabled` is `true`.
|
||||
This field is read-only and managed by the server.
|
||||
|
||||
If you previously called this endpoint to obtain the service account user ID for role mapping or other operations, retrieve the client representation instead:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
// Before:
|
||||
UserRepresentation sa = clientResource.getServiceAccountUser();
|
||||
String saUserId = sa.getId();
|
||||
|
||||
// After:
|
||||
String saUserId = clientResource.toRepresentation().getServiceAccountUserId();
|
||||
----
|
||||
|
||||
The `getServiceAccountUser()` method has also been removed from the Java and JavaScript admin client libraries.
|
||||
If you need the full user representation for the service account, use the user ID from the client representation to fetch it via the Users API.
|
||||
|
||||
// ------------------------ Notable changes ------------------------ //
|
||||
== Notable changes
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.ManagementPermissionReference;
|
||||
import org.keycloak.representations.idm.ManagementPermissionRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.idm.UserSessionRepresentation;
|
||||
|
||||
/**
|
||||
@@ -204,11 +203,6 @@ public interface ClientResource {
|
||||
@Path("optional-client-scopes/{clientScopeId}")
|
||||
void removeOptionalClientScope(@PathParam("clientScopeId") String clientScopeId);
|
||||
|
||||
@Path("/service-account-user")
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
UserRepresentation getServiceAccountUser();
|
||||
|
||||
@Path("nodes")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
||||
import type { RoleMappingPayload } from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
|
||||
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
|
||||
import { useAlerts, useFetch } from "@keycloak/keycloak-ui-shared";
|
||||
import { AlertVariant, PageSection } from "@patternfly/react-core";
|
||||
import { InfoCircleIcon } from "@patternfly/react-icons";
|
||||
@@ -27,20 +26,24 @@ export const ServiceAccount = ({ client }: ServiceAccountProps) => {
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const { realm } = useRealm();
|
||||
|
||||
const [serviceAccount, setServiceAccount] = useState<UserRepresentation>();
|
||||
const [serviceAccountUserId, setServiceAccountUserId] = useState<string>();
|
||||
|
||||
const { hasAccess } = useAccess();
|
||||
const hasManageClients = hasAccess("manage-clients");
|
||||
|
||||
useFetch(
|
||||
() =>
|
||||
adminClient.clients.getServiceAccountUser({
|
||||
async () => {
|
||||
const freshClient = await adminClient.clients.findOne({
|
||||
id: client.id!,
|
||||
}),
|
||||
(serviceAccount) => setServiceAccount(serviceAccount),
|
||||
});
|
||||
return freshClient?.serviceAccountUserId;
|
||||
},
|
||||
(id) => setServiceAccountUserId(id),
|
||||
[],
|
||||
);
|
||||
|
||||
const serviceAccountUsername = `service-account-${client.clientId}`;
|
||||
|
||||
const assignRoles = async (rows: Row[]) => {
|
||||
try {
|
||||
const realmRoles = rows
|
||||
@@ -48,7 +51,7 @@ export const ServiceAccount = ({ client }: ServiceAccountProps) => {
|
||||
.map((row) => row.role as RoleMappingPayload)
|
||||
.flat();
|
||||
await adminClient.users.addRealmRoleMappings({
|
||||
id: serviceAccount?.id!,
|
||||
id: serviceAccountUserId!,
|
||||
roles: realmRoles,
|
||||
});
|
||||
await Promise.all(
|
||||
@@ -56,7 +59,7 @@ export const ServiceAccount = ({ client }: ServiceAccountProps) => {
|
||||
.filter((row) => row.client !== undefined)
|
||||
.map((row) =>
|
||||
adminClient.users.addClientRoleMappings({
|
||||
id: serviceAccount?.id!,
|
||||
id: serviceAccountUserId!,
|
||||
clientUniqueId: row.client!.id!,
|
||||
roles: [row.role as RoleMappingPayload],
|
||||
}),
|
||||
@@ -67,7 +70,7 @@ export const ServiceAccount = ({ client }: ServiceAccountProps) => {
|
||||
addError("roleMappingUpdatedError", error);
|
||||
}
|
||||
};
|
||||
return serviceAccount ? (
|
||||
return serviceAccountUserId ? (
|
||||
<>
|
||||
<PageSection className="pf-v5-u-pb-0">
|
||||
<InfoCircleIcon className="pf-v5-c-alert__icon keycloak--service-account--info-text" />
|
||||
@@ -75,16 +78,16 @@ export const ServiceAccount = ({ client }: ServiceAccountProps) => {
|
||||
<Trans i18nKey="manageServiceAccountUser">
|
||||
{""}
|
||||
<Link
|
||||
to={toUser({ realm, id: serviceAccount.id!, tab: "settings" })}
|
||||
to={toUser({ realm, id: serviceAccountUserId, tab: "settings" })}
|
||||
>
|
||||
{{ link: serviceAccount.username }}
|
||||
{{ link: serviceAccountUsername }}
|
||||
</Link>
|
||||
</Trans>
|
||||
</span>
|
||||
</PageSection>
|
||||
<RoleMapping
|
||||
name={client.clientId!}
|
||||
id={serviceAccount.id!}
|
||||
id={serviceAccountUserId}
|
||||
type="users"
|
||||
isManager={hasManageClients || client.access?.configure}
|
||||
save={assignRoles}
|
||||
|
||||
@@ -40,6 +40,7 @@ export default interface ClientRepresentation {
|
||||
rootUrl?: string;
|
||||
secret?: string;
|
||||
serviceAccountsEnabled?: boolean;
|
||||
serviceAccountUserId?: string;
|
||||
standardFlowEnabled?: boolean;
|
||||
surrogateAuthRequired?: boolean;
|
||||
webOrigins?: string[];
|
||||
|
||||
@@ -161,19 +161,6 @@ export class Clients extends Resource<{ realm?: string }> {
|
||||
urlParamKeys: ["id", "roleName"],
|
||||
});
|
||||
|
||||
/**
|
||||
* Service account user
|
||||
*/
|
||||
|
||||
public getServiceAccountUser = this.makeRequest<
|
||||
{ id: string },
|
||||
UserRepresentation
|
||||
>({
|
||||
method: "GET",
|
||||
path: "/{id}/service-account-user",
|
||||
urlParamKeys: ["id"],
|
||||
});
|
||||
|
||||
/**
|
||||
* Client secret
|
||||
*/
|
||||
|
||||
@@ -257,13 +257,12 @@ describe("Clients", () => {
|
||||
expect(typeof installationProvider).to.be.equal("string");
|
||||
});
|
||||
|
||||
it("get service account user", async () => {
|
||||
const serviceAccountUser =
|
||||
await kcAdminClient.clients.getServiceAccountUser({
|
||||
id: currentClient.id!,
|
||||
});
|
||||
it("get service account user id", async () => {
|
||||
const client = await kcAdminClient.clients.findOne({
|
||||
id: currentClient.id!,
|
||||
});
|
||||
|
||||
expect(serviceAccountUser).to.be.ok;
|
||||
expect(client?.serviceAccountUserId).to.be.ok;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -147,10 +147,10 @@ public class KcAdmV2KeystoreCLITest {
|
||||
.substring(response.getLocation().getPath().lastIndexOf('/') + 1);
|
||||
response.close();
|
||||
|
||||
var serviceAccountUser = adminClient.realm("master").clients()
|
||||
.get(clientUuid).getServiceAccountUser();
|
||||
var serviceAccountUserId = adminClient.realm("master").clients()
|
||||
.get(clientUuid).toRepresentation().getServiceAccountUserId();
|
||||
var adminRole = adminClient.realm("master").roles().get("admin").toRepresentation();
|
||||
adminClient.realm("master").users().get(serviceAccountUser.getId())
|
||||
adminClient.realm("master").users().get(serviceAccountUserId)
|
||||
.roles().realmLevel().add(java.util.List.of(adminRole));
|
||||
|
||||
try {
|
||||
|
||||
@@ -379,13 +379,13 @@ public class AdminGroupProtectionTest {
|
||||
realm.cleanup().add(r -> r.clients().get(clientDbId).remove());
|
||||
|
||||
String regularGroupId = getGroupId(REGULAR_GROUP);
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(clientDbId).getServiceAccountUser();
|
||||
realm.admin().users().get(serviceAccount.getId()).joinGroup(regularGroupId);
|
||||
String serviceAccountUserId = realm.admin().clients().get(clientDbId).toRepresentation().getServiceAccountUserId();
|
||||
realm.admin().users().get(serviceAccountUserId).joinGroup(regularGroupId);
|
||||
|
||||
Group group = client.groups().get(regularGroupId, List.of("members"));
|
||||
List<org.keycloak.scim.resource.group.Member> members = group.getMembers();
|
||||
if (members != null) {
|
||||
boolean exposed = members.stream().anyMatch(m -> serviceAccount.getId().equals(m.getValue()));
|
||||
boolean exposed = members.stream().anyMatch(m -> serviceAccountUserId.equals(m.getValue()));
|
||||
assertFalse(exposed, "Service account must not appear in SCIM group members");
|
||||
}
|
||||
}
|
||||
@@ -405,11 +405,11 @@ public class AdminGroupProtectionTest {
|
||||
realm.cleanup().add(r -> r.clients().get(clientDbId).remove());
|
||||
|
||||
String regularGroupId = getGroupId(REGULAR_GROUP);
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(clientDbId).getServiceAccountUser();
|
||||
String serviceAccountUserId = realm.admin().clients().get(clientDbId).toRepresentation().getServiceAccountUserId();
|
||||
|
||||
try {
|
||||
client.groups().patch(regularGroupId, PatchRequest.create()
|
||||
.add("members", serviceAccount.getId())
|
||||
.add("members", serviceAccountUserId)
|
||||
.build());
|
||||
fail("Should not be able to add a service account to a group via SCIM PATCH");
|
||||
} catch (ScimClientException sce) {
|
||||
@@ -432,14 +432,14 @@ public class AdminGroupProtectionTest {
|
||||
realm.cleanup().add(r -> r.clients().get(clientDbId).remove());
|
||||
|
||||
String regularGroupId = getGroupId(REGULAR_GROUP);
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(clientDbId).getServiceAccountUser();
|
||||
String serviceAccountUserId = realm.admin().clients().get(clientDbId).toRepresentation().getServiceAccountUserId();
|
||||
|
||||
// Add service account to group via Admin API (bypassing SCIM protection)
|
||||
realm.admin().users().get(serviceAccount.getId()).joinGroup(regularGroupId);
|
||||
realm.admin().users().get(serviceAccountUserId).joinGroup(regularGroupId);
|
||||
|
||||
try {
|
||||
client.groups().patch(regularGroupId, PatchRequest.create()
|
||||
.remove("members[value eq \"" + serviceAccount.getId() + "\"]")
|
||||
.remove("members[value eq \"" + serviceAccountUserId + "\"]")
|
||||
.build());
|
||||
fail("Should not be able to remove a service account from a group via SCIM PATCH");
|
||||
} catch (ScimClientException sce) {
|
||||
@@ -447,7 +447,7 @@ public class AdminGroupProtectionTest {
|
||||
}
|
||||
|
||||
// Verify service account is still in the group (operation was rejected)
|
||||
UserResource userResource = realm.admin().users().get(serviceAccount.getId());
|
||||
UserResource userResource = realm.admin().users().get(serviceAccountUserId);
|
||||
List<String> groups = Optional.ofNullable(userResource.groups()).orElse(List.of()).stream().map(GroupRepresentation::getId).toList();
|
||||
assertTrue(groups.contains(regularGroupId), "Service account should still be in group after failed PATCH");
|
||||
}
|
||||
@@ -474,12 +474,12 @@ public class AdminGroupProtectionTest {
|
||||
String groupId = group.getId();
|
||||
realm.cleanup().add(r -> r.groups().group(groupId).remove());
|
||||
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(clientDbId).getServiceAccountUser();
|
||||
String serviceAccountUserId = realm.admin().clients().get(clientDbId).toRepresentation().getServiceAccountUserId();
|
||||
// Add only the service account to the group via Admin API (bypassing SCIM protection)
|
||||
realm.admin().users().get(serviceAccount.getId()).joinGroup(groupId);
|
||||
realm.admin().users().get(serviceAccountUserId).joinGroup(groupId);
|
||||
|
||||
// A service account must not be discoverable by its id (eq)
|
||||
assertGroupNotMatched(groupId, ResourceFilter.filter().eq("members.value", serviceAccount.getId()).build(),
|
||||
assertGroupNotMatched(groupId, ResourceFilter.filter().eq("members.value", serviceAccountUserId).build(),
|
||||
"Service account must not be discoverable via members.value eq");
|
||||
// A group whose only member is a service account must appear empty (pr)
|
||||
assertGroupNotMatched(groupId, ResourceFilter.filter().pr("members").build(),
|
||||
|
||||
@@ -309,10 +309,10 @@ public class AuthorizationTest extends AbstractScimTest {
|
||||
GroupRepresentation group = createGroup();
|
||||
managedUser.admin().joinGroup(group.getId());
|
||||
ClientRepresentation client = getScimClient();
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(client.getId()).getServiceAccountUser();
|
||||
String serviceAccountUserId = realm.admin().clients().get(client.getId()).toRepresentation().getServiceAccountUserId();
|
||||
UserPolicyRepresentation policy = new UserPolicyRepresentation();
|
||||
policy.setName("Allow SCIM access");
|
||||
policy.addUser(serviceAccount.getId());
|
||||
policy.addUser(serviceAccountUserId);
|
||||
ClientResource permissionClient = realm.admin().clients().get(realm.admin().clients().findByClientId(Constants.ADMIN_PERMISSIONS_CLIENT_ID).get(0).getId());
|
||||
permissionClient.authorization().policies().user().create(policy).close();
|
||||
ScopePermissionRepresentation permission = new ScopePermissionRepresentation();
|
||||
@@ -338,10 +338,10 @@ public class AuthorizationTest extends AbstractScimTest {
|
||||
|
||||
GroupRepresentation group = createGroup();
|
||||
ClientRepresentation client = getScimClient();
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(client.getId()).getServiceAccountUser();
|
||||
String serviceAccountUserId = realm.admin().clients().get(client.getId()).toRepresentation().getServiceAccountUserId();
|
||||
UserPolicyRepresentation policy = new UserPolicyRepresentation();
|
||||
policy.setName("Allow SCIM access");
|
||||
policy.addUser(serviceAccount.getId());
|
||||
policy.addUser(serviceAccountUserId);
|
||||
ClientResource permissionClient = realm.admin().clients().get(realm.admin().clients().findByClientId(Constants.ADMIN_PERMISSIONS_CLIENT_ID).get(0).getId());
|
||||
permissionClient.authorization().policies().user().create(policy).close();
|
||||
ScopePermissionRepresentation permission = new ScopePermissionRepresentation();
|
||||
@@ -517,12 +517,12 @@ public class AuthorizationTest extends AbstractScimTest {
|
||||
|
||||
GroupRepresentation group = createGroup();
|
||||
ClientRepresentation client = getScimClient();
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(client.getId()).getServiceAccountUser();
|
||||
String serviceAccountUserId = realm.admin().clients().get(client.getId()).toRepresentation().getServiceAccountUserId();
|
||||
|
||||
// Create policy for the SCIM service account
|
||||
UserPolicyRepresentation policy = new UserPolicyRepresentation();
|
||||
policy.setName("Allow SCIM user management");
|
||||
policy.addUser(serviceAccount.getId());
|
||||
policy.addUser(serviceAccountUserId);
|
||||
ClientResource permissionClient = realm.admin().clients().get(
|
||||
realm.admin().clients().findByClientId(Constants.ADMIN_PERMISSIONS_CLIENT_ID).get(0).getId());
|
||||
permissionClient.authorization().policies().user().create(policy).close();
|
||||
@@ -622,12 +622,12 @@ public class AuthorizationTest extends AbstractScimTest {
|
||||
|
||||
GroupRepresentation group = createGroup();
|
||||
ClientRepresentation client = getScimClient();
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(client.getId()).getServiceAccountUser();
|
||||
String serviceAccountUserId = realm.admin().clients().get(client.getId()).toRepresentation().getServiceAccountUserId();
|
||||
|
||||
// Create policy for the SCIM service account
|
||||
UserPolicyRepresentation policy = new UserPolicyRepresentation();
|
||||
policy.setName("Allow SCIM access");
|
||||
policy.addUser(serviceAccount.getId());
|
||||
policy.addUser(serviceAccountUserId);
|
||||
ClientResource permissionClient = realm.admin().clients().get(
|
||||
realm.admin().clients().findByClientId(Constants.ADMIN_PERMISSIONS_CLIENT_ID).get(0).getId());
|
||||
permissionClient.authorization().policies().user().create(policy).close();
|
||||
@@ -664,12 +664,12 @@ public class AuthorizationTest extends AbstractScimTest {
|
||||
managedUser.admin().joinGroup(group.getId());
|
||||
|
||||
ClientRepresentation client = getScimClient();
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(client.getId()).getServiceAccountUser();
|
||||
String serviceAccountUserId = realm.admin().clients().get(client.getId()).toRepresentation().getServiceAccountUserId();
|
||||
|
||||
// Create policy for the SCIM service account
|
||||
UserPolicyRepresentation policy = new UserPolicyRepresentation();
|
||||
policy.setName("Allow SCIM access");
|
||||
policy.addUser(serviceAccount.getId());
|
||||
policy.addUser(serviceAccountUserId);
|
||||
ClientResource permissionClient = realm.admin().clients().get(
|
||||
realm.admin().clients().findByClientId(Constants.ADMIN_PERMISSIONS_CLIENT_ID).get(0).getId());
|
||||
permissionClient.authorization().policies().user().create(policy).close();
|
||||
@@ -700,12 +700,12 @@ public class AuthorizationTest extends AbstractScimTest {
|
||||
managedUser.admin().joinGroup(group.getId());
|
||||
|
||||
ClientRepresentation client = getScimClient();
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(client.getId()).getServiceAccountUser();
|
||||
String serviceAccountUserId = realm.admin().clients().get(client.getId()).toRepresentation().getServiceAccountUserId();
|
||||
|
||||
// Create policy for the SCIM service account
|
||||
UserPolicyRepresentation policy = new UserPolicyRepresentation();
|
||||
policy.setName("Allow SCIM access");
|
||||
policy.addUser(serviceAccount.getId());
|
||||
policy.addUser(serviceAccountUserId);
|
||||
ClientResource permissionClient = realm.admin().clients().get(
|
||||
realm.admin().clients().findByClientId(Constants.ADMIN_PERMISSIONS_CLIENT_ID).get(0).getId());
|
||||
permissionClient.authorization().policies().user().create(policy).close();
|
||||
@@ -741,12 +741,12 @@ public class AuthorizationTest extends AbstractScimTest {
|
||||
|
||||
GroupRepresentation group = createGroup();
|
||||
ClientRepresentation client = getScimClient();
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(client.getId()).getServiceAccountUser();
|
||||
String serviceAccountUserId = realm.admin().clients().get(client.getId()).toRepresentation().getServiceAccountUserId();
|
||||
|
||||
// Create policy for the SCIM service account
|
||||
UserPolicyRepresentation policy = new UserPolicyRepresentation();
|
||||
policy.setName("Allow SCIM access");
|
||||
policy.addUser(serviceAccount.getId());
|
||||
policy.addUser(serviceAccountUserId);
|
||||
ClientResource permissionClient = realm.admin().clients().get(
|
||||
realm.admin().clients().findByClientId(Constants.ADMIN_PERMISSIONS_CLIENT_ID).get(0).getId());
|
||||
permissionClient.authorization().policies().user().create(policy).close();
|
||||
@@ -797,12 +797,12 @@ public class AuthorizationTest extends AbstractScimTest {
|
||||
|
||||
GroupRepresentation group = createGroup();
|
||||
ClientRepresentation client = getScimClient();
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(client.getId()).getServiceAccountUser();
|
||||
String serviceAccountUserId = realm.admin().clients().get(client.getId()).toRepresentation().getServiceAccountUserId();
|
||||
|
||||
// Create policy for the SCIM service account
|
||||
UserPolicyRepresentation policy = new UserPolicyRepresentation();
|
||||
policy.setName("Allow SCIM access");
|
||||
policy.addUser(serviceAccount.getId());
|
||||
policy.addUser(serviceAccountUserId);
|
||||
ClientResource permissionClient = realm.admin().clients().get(
|
||||
realm.admin().clients().findByClientId(Constants.ADMIN_PERMISSIONS_CLIENT_ID).get(0).getId());
|
||||
permissionClient.authorization().policies().user().create(policy).close();
|
||||
@@ -1115,11 +1115,11 @@ public class AuthorizationTest extends AbstractScimTest {
|
||||
clientDbId = ApiUtil.getCreatedId(response);
|
||||
}
|
||||
|
||||
UserRepresentation serviceAccountUser = realm.admin().clients().get(clientDbId).getServiceAccountUser();
|
||||
String serviceAccountUserId = realm.admin().clients().get(clientDbId).toRepresentation().getServiceAccountUserId();
|
||||
ClientRepresentation realmMgmt = realm.admin().clients().findByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID).get(0);
|
||||
RoleRepresentation manageUsersRole = realm.admin().clients().get(realmMgmt.getId()).roles()
|
||||
.get(AdminRoles.MANAGE_USERS).toRepresentation();
|
||||
realm.admin().users().get(serviceAccountUser.getId()).roles()
|
||||
realm.admin().users().get(serviceAccountUserId).roles()
|
||||
.clientLevel(realmMgmt.getId()).add(List.of(manageUsersRole));
|
||||
realm.admin().clients().get(clientDbId).getScopeMappings()
|
||||
.clientLevel(realmMgmt.getId()).add(List.of(manageUsersRole));
|
||||
@@ -1210,11 +1210,11 @@ public class AuthorizationTest extends AbstractScimTest {
|
||||
try {
|
||||
ClientRepresentation masterRealmMgmt = adminClient.realm("master").clients()
|
||||
.findByClientId("master-realm").get(0);
|
||||
UserRepresentation scimServiceAccount = adminClient.realm("master").clients()
|
||||
.get(scimMasterClient.getId()).getServiceAccountUser();
|
||||
String scimServiceAccountUserId = adminClient.realm("master").clients()
|
||||
.get(scimMasterClient.getId()).toRepresentation().getServiceAccountUserId();
|
||||
RoleRepresentation manageUsersRole = adminClient.realm("master").clients()
|
||||
.get(masterRealmMgmt.getId()).roles().get(AdminRoles.MANAGE_USERS).toRepresentation();
|
||||
adminClient.realm("master").users().get(scimServiceAccount.getId())
|
||||
adminClient.realm("master").users().get(scimServiceAccountUserId)
|
||||
.roles().clientLevel(masterRealmMgmt.getId()).add(List.of(manageUsersRole));
|
||||
adminClient.realm("master").clients().get(scimMasterClient.getId())
|
||||
.getScopeMappings().clientLevel(masterRealmMgmt.getId()).add(List.of(manageUsersRole));
|
||||
@@ -1490,7 +1490,8 @@ public class AuthorizationTest extends AbstractScimTest {
|
||||
}
|
||||
|
||||
private UserRepresentation getServiceAccount() {
|
||||
return realm.admin().clients().get(getScimClient().getId()).getServiceAccountUser();
|
||||
String saUserId = realm.admin().clients().get(getScimClient().getId()).toRepresentation().getServiceAccountUserId();
|
||||
return realm.admin().users().get(saUserId).toRepresentation();
|
||||
}
|
||||
|
||||
private ScopePermissionRepresentation createPermission(String resourceType, Set<String> resources, Set<String> scopes, String policyName) {
|
||||
@@ -1526,7 +1527,8 @@ public class AuthorizationTest extends AbstractScimTest {
|
||||
|
||||
private void grantAdminRole(String role) {
|
||||
ClientRepresentation clientRep = getScimClient();
|
||||
UserRepresentation serviceAccountUser = realm.admin().clients().get(clientRep.getId()).getServiceAccountUser();
|
||||
String serviceAccountUserId = realm.admin().clients().get(clientRep.getId()).toRepresentation().getServiceAccountUserId();
|
||||
UserRepresentation serviceAccountUser = realm.admin().users().get(serviceAccountUserId).toRepresentation();
|
||||
grantAdminRole(role, serviceAccountUser, clientRep.getId());
|
||||
}
|
||||
|
||||
@@ -1669,7 +1671,7 @@ public class AuthorizationTest extends AbstractScimTest {
|
||||
ClientRepresentation realmMgmt = realm.admin().clients().findByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID).get(0);
|
||||
RoleRepresentation role = realm.admin().clients().get(realmMgmt.getId()).roles().get(name).toRepresentation();
|
||||
ClientRepresentation clientRep = getScimClient();
|
||||
UserRepresentation serviceAccountUser = realm.admin().clients().get(clientRep.getId()).getServiceAccountUser();
|
||||
realm.admin().users().get(serviceAccountUser.getId()).roles().clientLevel(realmMgmt.getId()).remove(List.of(role));
|
||||
String serviceAccountUserId = realm.admin().clients().get(clientRep.getId()).toRepresentation().getServiceAccountUserId();
|
||||
realm.admin().users().get(serviceAccountUserId).roles().clientLevel(realmMgmt.getId()).remove(List.of(role));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.scim.client.ResourceFilter;
|
||||
import org.keycloak.scim.client.ScimClientException;
|
||||
import org.keycloak.scim.protocol.request.PatchRequest;
|
||||
@@ -1167,12 +1166,12 @@ public class GroupTest extends AbstractScimTest {
|
||||
}
|
||||
realm.cleanup().add(r -> r.clients().get(clientDbId).remove());
|
||||
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(clientDbId).getServiceAccountUser();
|
||||
String serviceAccountUserId = realm.admin().clients().get(clientDbId).toRepresentation().getServiceAccountUserId();
|
||||
|
||||
// Try to create a group with service account in members
|
||||
Group newGroup = new Group();
|
||||
newGroup.setDisplayName("test-sa-group-" + KeycloakModelUtils.generateId());
|
||||
newGroup.addMember(serviceAccount.getId());
|
||||
newGroup.addMember(serviceAccountUserId);
|
||||
|
||||
try {
|
||||
client.groups().create(newGroup);
|
||||
@@ -1201,10 +1200,10 @@ public class GroupTest extends AbstractScimTest {
|
||||
group.setDisplayName("test-group-" + KeycloakModelUtils.generateId());
|
||||
group = client.groups().create(group);
|
||||
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(clientDbId).getServiceAccountUser();
|
||||
String serviceAccountUserId = realm.admin().clients().get(clientDbId).toRepresentation().getServiceAccountUserId();
|
||||
|
||||
// Add both service account and regular user to group via Admin API
|
||||
realm.admin().users().get(serviceAccount.getId()).joinGroup(group.getId());
|
||||
realm.admin().users().get(serviceAccountUserId).joinGroup(group.getId());
|
||||
realm.admin().users().get(regularUser.getId()).joinGroup(group.getId());
|
||||
|
||||
// Verify regular user is removed from group
|
||||
@@ -1228,7 +1227,7 @@ public class GroupTest extends AbstractScimTest {
|
||||
assertNull(updated.getMembers(), "Members list should be null");
|
||||
|
||||
// Verify service account is still in the group (using Admin API, since SCIM filters them out)
|
||||
List<String> groupIds = Optional.ofNullable(realm.admin().users().get(serviceAccount.getId()).groups()).orElse(List.of()).stream().map(GroupRepresentation::getId).toList();
|
||||
List<String> groupIds = Optional.ofNullable(realm.admin().users().get(serviceAccountUserId).groups()).orElse(List.of()).stream().map(GroupRepresentation::getId).toList();
|
||||
assertTrue(groupIds.contains(group.getId()), "Service account should still be in group");
|
||||
}
|
||||
|
||||
|
||||
@@ -417,8 +417,8 @@ public class UserTest extends AbstractScimTest {
|
||||
}
|
||||
realm.cleanup().add(realm -> realm.clients().get(targetClient.getId()).remove());
|
||||
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(targetClient.getId()).getServiceAccountUser();
|
||||
User actual = client.users().get(serviceAccount.getId());
|
||||
String serviceAccountUserId = realm.admin().clients().get(targetClient.getId()).toRepresentation().getServiceAccountUserId();
|
||||
User actual = client.users().get(serviceAccountUserId);
|
||||
|
||||
assertNull(actual);
|
||||
}
|
||||
@@ -437,8 +437,8 @@ public class UserTest extends AbstractScimTest {
|
||||
}
|
||||
realm.cleanup().add(realm -> realm.clients().get(targetClient.getId()).remove());
|
||||
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(targetClient.getId()).getServiceAccountUser();
|
||||
ListResponse<User> response = client.users().getAll(ResourceFilter.filter().eq("id", serviceAccount.getId()).build());
|
||||
String serviceAccountUserId = realm.admin().clients().get(targetClient.getId()).toRepresentation().getServiceAccountUserId();
|
||||
ListResponse<User> response = client.users().getAll(ResourceFilter.filter().eq("id", serviceAccountUserId).build());
|
||||
|
||||
assertEquals(0, response.getTotalResults());
|
||||
}
|
||||
@@ -457,7 +457,8 @@ public class UserTest extends AbstractScimTest {
|
||||
}
|
||||
realm.cleanup().add(realm -> realm.clients().get(targetClient.getId()).remove());
|
||||
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(targetClient.getId()).getServiceAccountUser();
|
||||
String serviceAccountUserId = realm.admin().clients().get(targetClient.getId()).toRepresentation().getServiceAccountUserId();
|
||||
UserRepresentation serviceAccount = realm.admin().users().get(serviceAccountUserId).toRepresentation();
|
||||
User update = new User();
|
||||
update.setId(serviceAccount.getId());
|
||||
update.setUserName(serviceAccount.getUsername());
|
||||
@@ -484,10 +485,10 @@ public class UserTest extends AbstractScimTest {
|
||||
}
|
||||
realm.cleanup().add(realm -> realm.clients().get(targetClient.getId()).remove());
|
||||
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(targetClient.getId()).getServiceAccountUser();
|
||||
String serviceAccountUserId = realm.admin().clients().get(targetClient.getId()).toRepresentation().getServiceAccountUserId();
|
||||
|
||||
try {
|
||||
client.users().delete(serviceAccount.getId());
|
||||
client.users().delete(serviceAccountUserId);
|
||||
fail("Should not be able to delete a service account via SCIM");
|
||||
} catch (ScimClientException sce) {
|
||||
assertEquals(Status.NOT_FOUND.getStatusCode(), sce.getError().getStatusInt());
|
||||
@@ -508,10 +509,10 @@ public class UserTest extends AbstractScimTest {
|
||||
}
|
||||
realm.cleanup().add(realm -> realm.clients().get(targetClient.getId()).remove());
|
||||
|
||||
UserRepresentation serviceAccount = realm.admin().clients().get(targetClient.getId()).getServiceAccountUser();
|
||||
String serviceAccountUserId = realm.admin().clients().get(targetClient.getId()).toRepresentation().getServiceAccountUserId();
|
||||
|
||||
try {
|
||||
client.users().patch(serviceAccount.getId(), PatchRequest.create()
|
||||
client.users().patch(serviceAccountUserId, PatchRequest.create()
|
||||
.add("name.givenName", "modified")
|
||||
.build());
|
||||
fail("Should not be able to patch a service account via SCIM");
|
||||
|
||||
@@ -10,7 +10,6 @@ import org.keycloak.models.Constants;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.scim.client.ScimClient;
|
||||
import org.keycloak.scim.client.authorization.OAuth2Bearer;
|
||||
import org.keycloak.testframework.injection.DependenciesBuilder;
|
||||
@@ -58,10 +57,10 @@ public class ScimClientSupplier implements Supplier<ScimClient, InjectScimClient
|
||||
.enabled(true)
|
||||
.build())) {
|
||||
String id = ApiUtil.getCreatedId(response);
|
||||
UserRepresentation serviceAccountUser = managedRealm.admin().clients().get(id).getServiceAccountUser();
|
||||
String serviceAccountUserId = managedRealm.admin().clients().get(id).toRepresentation().getServiceAccountUserId();
|
||||
ClientRepresentation realmMgmtClient = managedRealm.admin().clients().findByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID).get(0);
|
||||
RoleRepresentation manageUsersRole = managedRealm.admin().clients().get(realmMgmtClient.getId()).roles().get(AdminRoles.MANAGE_USERS).toRepresentation();
|
||||
managedRealm.admin().users().get(serviceAccountUser.getId()).roles()
|
||||
managedRealm.admin().users().get(serviceAccountUserId).roles()
|
||||
.clientLevel(realmMgmtClient.getId())
|
||||
.add(List.of(manageUsersRole));
|
||||
managedRealm.admin().clients().get(id).getScopeMappings()
|
||||
|
||||
@@ -1588,4 +1588,11 @@ public class ModelToRepresentation {
|
||||
return toRepresentation(model, credentialType);
|
||||
}
|
||||
|
||||
public static void setServiceAccountUserId(KeycloakSession session, ClientModel client, ClientRepresentation rep) {
|
||||
UserModel serviceAccount = session.users().getServiceAccount(client);
|
||||
if (serviceAccount != null) {
|
||||
rep.setServiceAccountUserId(serviceAccount.getId());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,9 +21,13 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.connections.jpa.support.EntityManagers;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.PartialImportRepresentation;
|
||||
import org.keycloak.services.managers.ClientManager;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
|
||||
/**
|
||||
* This class manages the PartialImport handlers.
|
||||
@@ -65,7 +69,24 @@ public class PartialImportManager {
|
||||
results.addAllResults(partialImport.doImport(rep, realm, session));
|
||||
}
|
||||
|
||||
ensureServiceAccountsExist(results);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private void ensureServiceAccountsExist(PartialImportResults results) {
|
||||
ClientManager clientManager = new ClientManager(new RealmManager(session));
|
||||
for (PartialImportResult result : results.getResults()) {
|
||||
if (result.getResourceType() != ResourceType.CLIENT
|
||||
|| (result.getAction() != Action.ADDED && result.getAction() != Action.OVERWRITTEN)) {
|
||||
continue;
|
||||
}
|
||||
ClientRepresentation clientRep = (ClientRepresentation) result.getRepresentation();
|
||||
ClientModel client = realm.getClientByClientId(clientRep.getClientId());
|
||||
if (client != null && client.isServiceAccountsEnabled() && session.users().getServiceAccount(client) == null) {
|
||||
clientManager.enableServiceAccount(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.ModelValidationException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
@@ -76,7 +75,6 @@ import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.ManagementPermissionReference;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.idm.UserSessionRepresentation;
|
||||
import org.keycloak.services.ErrorResponse;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
@@ -225,6 +223,10 @@ public class ClientResource {
|
||||
|
||||
representation.setAccess(auth.clients().getAccess(client));
|
||||
|
||||
if (client.isServiceAccountsEnabled()) {
|
||||
ModelToRepresentation.setServiceAccountUserId(session, client, representation);
|
||||
}
|
||||
|
||||
return representation;
|
||||
}
|
||||
|
||||
@@ -522,26 +524,6 @@ public class ClientResource {
|
||||
return new ClientScopeEvaluateResource(session, session.getContext().getUri(), realm, auth, client, clientConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user dedicated to the service account
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("service-account-user")
|
||||
@GET
|
||||
@NoCache
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS)
|
||||
@Operation( summary = "Get a user dedicated to the service account")
|
||||
public UserRepresentation getServiceAccountUser() {
|
||||
auth.clients().requireView(client);
|
||||
|
||||
UserModel user = new ClientManager(new RealmManager(session)).getServiceAccountUser(client)
|
||||
.orElseThrow(() -> new BadRequestException("Service account not enabled for the client '" + client.getClientId() + "'"));
|
||||
|
||||
return ModelToRepresentation.toRepresentation(session, realm, user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push the client's revocation policy to its admin URL
|
||||
*
|
||||
|
||||
@@ -135,6 +135,9 @@ public class ClientsResource {
|
||||
StripSecretsUtils.stripClient(representation);
|
||||
}
|
||||
representation.setAccess(auth.clients().getAccess(c));
|
||||
if (c.isServiceAccountsEnabled()) {
|
||||
ModelToRepresentation.setServiceAccountUserId(session, c, representation);
|
||||
}
|
||||
return representation;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.ssf.Ssf;
|
||||
import org.keycloak.ssf.event.caep.CaepCredentialChange;
|
||||
import org.keycloak.ssf.event.caep.CaepSessionRevoked;
|
||||
@@ -1375,14 +1374,14 @@ public class SsfTransmitterEventEmitterTests {
|
||||
|
||||
protected void grantRoleToServiceAccount(String mgmtClientId, String targetClientId, String roleName) {
|
||||
ClientRepresentation mgmt = findClientByClientId(mgmtClientId);
|
||||
UserRepresentation saUser = realm.admin().clients().get(mgmt.getId()).getServiceAccountUser();
|
||||
String saUserId = realm.admin().clients().get(mgmt.getId()).toRepresentation().getServiceAccountUserId();
|
||||
|
||||
String targetUuid = findClientByClientId(targetClientId).getId();
|
||||
RoleRepresentation role = realm.admin().clients().get(targetUuid).roles().get(roleName).toRepresentation();
|
||||
|
||||
// The admin API is idempotent for add() — if the mapping already
|
||||
// exists it's a no-op, which is what we want for @BeforeEach.
|
||||
realm.admin().users().get(saUser.getId()).roles().clientLevel(targetUuid).add(List.of(role));
|
||||
realm.admin().users().get(saUserId).roles().clientLevel(targetUuid).add(List.of(role));
|
||||
}
|
||||
|
||||
protected ClientRepresentation findClientByClientId(String clientId) {
|
||||
|
||||
@@ -844,7 +844,8 @@ public class ClientTest {
|
||||
Response response = managedRealm.admin().clients().create(ClientBuilder.create().clientId("serviceClient").serviceAccountsEnabled(true).build());
|
||||
String id = ApiUtil.getCreatedId(response);
|
||||
managedRealm.cleanup().add(r -> r.clients().get(id).remove());
|
||||
UserRepresentation userRep = managedRealm.admin().clients().get(id).getServiceAccountUser();
|
||||
String saUserId = managedRealm.admin().clients().get(id).toRepresentation().getServiceAccountUserId();
|
||||
UserRepresentation userRep = managedRealm.admin().users().get(saUserId).toRepresentation();
|
||||
MatcherAssert.assertThat("service-account-serviceclient", Matchers.equalTo(userRep.getUsername()));
|
||||
// KEYCLOAK-11197 service accounts are no longer created with a placeholder e-mail.
|
||||
assertNull(userRep.getEmail());
|
||||
|
||||
@@ -293,7 +293,8 @@ public class ImpersonationTest {
|
||||
}
|
||||
managedRealm.cleanup().add(r -> r.clients().get(clientId).remove());
|
||||
|
||||
UserRepresentation user = AdminApiUtil.findClientByClientId(managedRealm.admin(), "service-account-cl").getServiceAccountUser();
|
||||
String saUserId = AdminApiUtil.findClientByClientId(managedRealm.admin(), "service-account-cl").toRepresentation().getServiceAccountUserId();
|
||||
UserRepresentation user = managedRealm.admin().users().get(saUserId).toRepresentation();
|
||||
user.setServiceAccountClientId("service-account-cl");
|
||||
|
||||
// add impersonation roles
|
||||
@@ -319,7 +320,8 @@ public class ImpersonationTest {
|
||||
}
|
||||
masterRealm.cleanup().add(r -> r.clients().get(clientId).remove());
|
||||
|
||||
UserRepresentation user = AdminApiUtil.findClientByClientId(masterRealm.admin(), "service-account-cl").getServiceAccountUser();
|
||||
String saUserId = AdminApiUtil.findClientByClientId(masterRealm.admin(), "service-account-cl").toRepresentation().getServiceAccountUserId();
|
||||
UserRepresentation user = masterRealm.admin().users().get(saUserId).toRepresentation();
|
||||
user.setServiceAccountClientId("service-account-cl");
|
||||
|
||||
// add impersonation roles
|
||||
|
||||
@@ -163,7 +163,6 @@ public class PermissionsTest extends AbstractPermissionsTest {
|
||||
invoke(realm -> realm.clients().get(foo.getId()).regenerateRegistrationAccessToken(), Resource.CLIENT, true);
|
||||
invoke(realm -> realm.clients().get(foo.getId()).getSecret(), Resource.CLIENT, true);
|
||||
invoke(realm -> realm.clients().get(foo.getId()).getClientRotatedSecret(), Resource.CLIENT, true);
|
||||
invoke(realm -> realm.clients().get(foo.getId()).getServiceAccountUser(), Resource.CLIENT, false);
|
||||
invoke(realm -> realm.clients().get(foo.getId()).pushRevocation(), Resource.CLIENT, true);
|
||||
invoke(realm -> realm.clients().get(foo.getId()).getApplicationSessionCount(), Resource.CLIENT, false);
|
||||
invoke(realm -> realm.clients().get(foo.getId()).getUserSessions(0, 100), Resource.CLIENT, false);
|
||||
|
||||
@@ -19,11 +19,18 @@ package org.keycloak.tests.admin.client;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.admin.client.resource.ClientScopeResource;
|
||||
import jakarta.ws.rs.ForbiddenException;
|
||||
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.common.constants.ServiceAccountConstants;
|
||||
import org.keycloak.models.AdminRoles;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testframework.admin.AdminClientFactory;
|
||||
import org.keycloak.testframework.annotations.InjectAdminClientFactory;
|
||||
import org.keycloak.testframework.annotations.InjectClient;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
@@ -33,6 +40,9 @@ import org.keycloak.testframework.realm.ClientBuilder;
|
||||
import org.keycloak.testframework.realm.ClientConfig;
|
||||
import org.keycloak.testframework.realm.ManagedClient;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.RealmBuilder;
|
||||
import org.keycloak.testframework.realm.RealmConfig;
|
||||
import org.keycloak.testframework.realm.UserBuilder;
|
||||
import org.keycloak.tests.utils.admin.AdminApiUtil;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
|
||||
@@ -47,7 +57,11 @@ import org.junit.jupiter.api.Test;
|
||||
@KeycloakIntegrationTest
|
||||
public class ServiceAccountClientTest {
|
||||
|
||||
@InjectRealm
|
||||
private static final String clientId = "service-account-client";
|
||||
private static final String VIEW_CLIENTS_USER = "view-clients-user";
|
||||
private static final String PASSWORD = "password";
|
||||
|
||||
@InjectRealm(config = ServiceAccountRealmConfig.class)
|
||||
ManagedRealm managedRealm;
|
||||
|
||||
@InjectClient(config = ServiceAccountClientConfig.class)
|
||||
@@ -56,11 +70,12 @@ public class ServiceAccountClientTest {
|
||||
@InjectOAuthClient
|
||||
OAuthClient oAuthClient;
|
||||
|
||||
private static final String clientId = "service-account-client";
|
||||
@InjectAdminClientFactory
|
||||
AdminClientFactory adminClientFactory;
|
||||
|
||||
@Test
|
||||
public void testServiceAccountEnableDisable() {
|
||||
ClientScopeResource serviceAccountScopeRsc = AdminApiUtil.findClientScopeByName(
|
||||
org.keycloak.admin.client.resource.ClientScopeResource serviceAccountScopeRsc = AdminApiUtil.findClientScopeByName(
|
||||
managedRealm.admin(), ServiceAccountConstants.SERVICE_ACCOUNT_SCOPE);
|
||||
Assertions.assertNotNull(serviceAccountScopeRsc);
|
||||
ClientScopeRepresentation serviceAccountScope = serviceAccountScopeRsc.toRepresentation();
|
||||
@@ -124,12 +139,35 @@ public class ServiceAccountClientTest {
|
||||
Assertions.assertNotNull(accessToken.getOtherClaims().get(ServiceAccountConstants.CLIENT_ADDRESS));
|
||||
|
||||
// remove the service account and client credentials should fail
|
||||
UserRepresentation serviceAccountUser = managedClient.admin().getServiceAccountUser();
|
||||
managedRealm.admin().users().delete(serviceAccountUser.getId()).close();
|
||||
String serviceAccountUserId = managedClient.admin().toRepresentation().getServiceAccountUserId();
|
||||
managedRealm.admin().users().delete(serviceAccountUserId).close();
|
||||
response = oAuthClient.doClientCredentialsGrantAccessTokenRequest();
|
||||
Assertions.assertEquals("invalid_request", response.getError());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testServiceAccountUserIdVisibleWithViewClientsOnly() {
|
||||
try (Keycloak viewClientsAdmin = adminClientFactory.create()
|
||||
.realm(managedRealm.getName())
|
||||
.username(VIEW_CLIENTS_USER)
|
||||
.password(PASSWORD)
|
||||
.clientId(Constants.ADMIN_CLI_CLIENT_ID)
|
||||
.build()) {
|
||||
|
||||
ClientRepresentation client = viewClientsAdmin.realm(managedRealm.getName())
|
||||
.clients().findByClientId(clientId).get(0);
|
||||
ClientResource clientResource = viewClientsAdmin.realm(managedRealm.getName())
|
||||
.clients().get(client.getId());
|
||||
|
||||
ClientRepresentation fullClient = clientResource.toRepresentation();
|
||||
Assertions.assertNotNull(fullClient.getServiceAccountUserId());
|
||||
|
||||
Assertions.assertThrows(ForbiddenException.class, () ->
|
||||
viewClientsAdmin.realm(managedRealm.getName())
|
||||
.users().get(fullClient.getServiceAccountUserId()).toRepresentation());
|
||||
}
|
||||
}
|
||||
|
||||
private static class ServiceAccountClientConfig implements ClientConfig {
|
||||
|
||||
@Override
|
||||
@@ -143,4 +181,19 @@ public class ServiceAccountClientTest {
|
||||
.publicClient(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ServiceAccountRealmConfig implements RealmConfig {
|
||||
|
||||
@Override
|
||||
public RealmBuilder configure(RealmBuilder realm) {
|
||||
realm.users(UserBuilder.create(VIEW_CLIENTS_USER)
|
||||
.password(PASSWORD)
|
||||
.email(VIEW_CLIENTS_USER + "@localhost")
|
||||
.firstName("View")
|
||||
.lastName("Clients")
|
||||
.clientRoles(Constants.REALM_MANAGEMENT_CLIENT_ID,
|
||||
AdminRoles.VIEW_CLIENTS));
|
||||
return realm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ public class PartialImportClientTest extends AbstractPartialImportTest {
|
||||
Assertions.assertEquals(1, doImport().getOverwritten());
|
||||
|
||||
ClientRepresentation client = managedRealm.admin().clients().findByClientId(CLIENT_SERVICE_ACCOUNT).get(0);
|
||||
Assertions.assertDoesNotThrow(() -> managedRealm.admin().clients().get(client.getId()).getServiceAccountUser());
|
||||
Assertions.assertNotNull(managedRealm.admin().clients().get(client.getId()).toRepresentation().getServiceAccountUserId());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -56,7 +56,6 @@ import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
|
||||
@@ -136,14 +135,14 @@ public class ClientRegistrationTest extends AbstractClientRegistrationTest {
|
||||
|
||||
ClientRepresentation rep = registerClient(clientRep, false);
|
||||
|
||||
UserRepresentation serviceAccountUser = managedRealm.admin().clients().get(rep.getId()).getServiceAccountUser();
|
||||
String serviceAccountUserId = managedRealm.admin().clients().get(rep.getId()).toRepresentation().getServiceAccountUserId();
|
||||
|
||||
assertNotNull(serviceAccountUser);
|
||||
assertNotNull(serviceAccountUserId);
|
||||
|
||||
deleteClient(rep);
|
||||
|
||||
try {
|
||||
managedRealm.admin().users().get(serviceAccountUser.getId()).toRepresentation();
|
||||
managedRealm.admin().users().get(serviceAccountUserId).toRepresentation();
|
||||
fail("Expected NotFoundException");
|
||||
} catch (NotFoundException e) {
|
||||
}
|
||||
@@ -997,9 +996,9 @@ public class ClientRegistrationTest extends AbstractClientRegistrationTest {
|
||||
Response response = masterRealm.admin().clients().create(testAdminClient);
|
||||
testAdminClient = masterRealm.admin().clients().get( ApiUtil.getCreatedId(response)).toRepresentation();
|
||||
|
||||
UserRepresentation serviceAccountUser = masterRealm.admin().clients().get(testAdminClient.getId()).getServiceAccountUser();
|
||||
String serviceAccountUserId = testAdminClient.getServiceAccountUserId();
|
||||
RoleRepresentation adminRole = masterRealm.admin().roles().get(AdminRoles.ADMIN).toRepresentation();
|
||||
masterRealm.admin().users().get(serviceAccountUser.getId()).roles().realmLevel().add(List.of(adminRole));
|
||||
masterRealm.admin().users().get(serviceAccountUserId).roles().realmLevel().add(List.of(adminRole));
|
||||
|
||||
oauth.client(TEST_ADMIN_CLIENT, TEST_ADMIN_CLIENT);
|
||||
oauth.realm(masterRealm.getName());
|
||||
|
||||
@@ -76,7 +76,7 @@ public class OIDCClientRegistrationCorsTest {
|
||||
RoleRepresentation createClientRoleRep = realm.admin().clients().get(realmManagementId).roles().get("create-client").toRepresentation();
|
||||
Response response = realm.admin().clients().create(ClientBuilder.create().clientId("testWithServiceAccount").secret("secret").serviceAccountsEnabled(true).webOrigins("https://origin1").build());
|
||||
String clientUuid = CreatedResponseUtil.getCreatedId(response);
|
||||
String serviceAccountUuid = realm.admin().clients().get(clientUuid).getServiceAccountUser().getId();
|
||||
String serviceAccountUuid = realm.admin().clients().get(clientUuid).toRepresentation().getServiceAccountUserId();
|
||||
realm.admin().users().get(serviceAccountUuid).roles().clientLevel(realmManagementId).add(List.of(createClientRoleRep));
|
||||
|
||||
String accessToken = oAuthClient.client("testWithServiceAccount", "secret").doClientCredentialsGrantAccessTokenRequest().getAccessToken();
|
||||
|
||||
@@ -83,7 +83,7 @@ public class SAMLClientRegistrationTest extends AbstractClientRegistrationTest {
|
||||
public void testSAMLEndpointCreateWithOIDCClient() throws Exception {
|
||||
ClientsResource clientsResource = managedRealm.admin().clients();
|
||||
ClientRepresentation oidcClient = clientsResource.findByClientId("oidc-client").get(0);
|
||||
String oidcClientServiceId = clientsResource.get(oidcClient.getId()).getServiceAccountUser().getId();
|
||||
String oidcClientServiceId = clientsResource.get(oidcClient.getId()).toRepresentation().getServiceAccountUserId();
|
||||
|
||||
String realmManagementId = clientsResource.findByClientId("realm-management").get(0).getId();
|
||||
RoleRepresentation role = clientsResource.get(realmManagementId).roles().get("create-client").toRepresentation();
|
||||
|
||||
@@ -554,7 +554,7 @@ public class ClientAgentDelegationTest {
|
||||
}
|
||||
|
||||
private String getServiceAccountUserId() {
|
||||
return actorApp.admin().getServiceAccountUser().getId();
|
||||
return actorApp.admin().toRepresentation().getServiceAccountUserId();
|
||||
}
|
||||
|
||||
private void logout(String refreshToken) {
|
||||
|
||||
@@ -282,7 +282,7 @@ public class ClientDelegationCibaTest {
|
||||
private String getServiceAccountUserId() {
|
||||
return realm.admin().clients().findByClientId(AGENT_CLIENT_ID).stream()
|
||||
.findFirst()
|
||||
.map(c -> realm.admin().clients().get(c.getId()).getServiceAccountUser().getId())
|
||||
.map(c -> realm.admin().clients().get(c.getId()).toRepresentation().getServiceAccountUserId())
|
||||
.orElseThrow(() -> new AssertionError("agent-app client not found"));
|
||||
}
|
||||
|
||||
|
||||
@@ -534,11 +534,11 @@ public class ClientDelegationTest {
|
||||
}
|
||||
|
||||
private String getServiceAccountUserId() {
|
||||
return agentApp.admin().getServiceAccountUser().getId();
|
||||
return agentApp.admin().toRepresentation().getServiceAccountUserId();
|
||||
}
|
||||
|
||||
private String getServiceAccountUserId(String clientUuid) {
|
||||
return realm.admin().clients().get(clientUuid).getServiceAccountUser().getId();
|
||||
return realm.admin().clients().get(clientUuid).toRepresentation().getServiceAccountUserId();
|
||||
}
|
||||
|
||||
private String createClient(String clientId, String clientSecret) {
|
||||
|
||||
@@ -327,7 +327,8 @@ public class StandardBaseTokenExchangeV2Test extends AbstractBaseTokenExchangeTe
|
||||
|
||||
@Test
|
||||
public void testExchangeUsingServiceAccount() {
|
||||
final UserRepresentation user = realm.admin().clients().get(subjectClient.getId()).getServiceAccountUser();
|
||||
String saUserId = realm.admin().clients().get(subjectClient.getId()).toRepresentation().getServiceAccountUserId();
|
||||
final UserRepresentation user = realm.admin().users().get(saUserId).toRepresentation();
|
||||
oauth.scope(null);
|
||||
oauth.client("subject-client", "secret");
|
||||
|
||||
|
||||
@@ -274,7 +274,8 @@ public class OID4VCredentialOfferPreAuthTest extends OID4VCIssuerTestBase {
|
||||
@Test
|
||||
public void testPreAuthOffer_ServiceAccountCanRedeemCode() {
|
||||
var ctx = new OID4VCTestContext(client, jwtTypeCredentialScope);
|
||||
UserRepresentation serviceAccount = testRealm.admin().clients().get(client.getId()).getServiceAccountUser();
|
||||
String saUserId = testRealm.admin().clients().get(client.getId()).toRepresentation().getServiceAccountUserId();
|
||||
UserRepresentation serviceAccount = testRealm.admin().users().get(saUserId).toRepresentation();
|
||||
UserResource serviceAccountResource = testRealm.admin().users().get(serviceAccount.getId());
|
||||
RoleRepresentation credentialOfferRole = testRealm.admin().roles()
|
||||
.get(CREDENTIAL_OFFER_CREATE.getName()).toRepresentation();
|
||||
|
||||
@@ -2141,7 +2141,7 @@ public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest
|
||||
realm.admin().clients().get(createdClient.getId()).addOptionalClientScope(orgScope.getId());
|
||||
|
||||
// make the service account user a member of the organization
|
||||
String serviceAccountUserId = realm.admin().clients().get(createdClient.getId()).getServiceAccountUser().getId();
|
||||
String serviceAccountUserId = realm.admin().clients().get(createdClient.getId()).toRepresentation().getServiceAccountUserId();
|
||||
realm.admin().organizations().get(orgA.getId()).members().addMember(serviceAccountUserId).close();
|
||||
|
||||
// first, verify organization claim IS present when organizations are enabled
|
||||
|
||||
@@ -152,12 +152,12 @@ public class GroupNamePolicyTest extends AbstractAuthzTest {
|
||||
}
|
||||
|
||||
RealmResource realm = getRealm();
|
||||
UserRepresentation serviceAccount = getClient().getServiceAccountUser();
|
||||
String serviceAccountUserId = getClient().toRepresentation().getServiceAccountUserId();
|
||||
GroupRepresentation rootGroup = realm.groups().groups().stream()
|
||||
.filter(group -> "Group A".equals(group.getName()))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
realm.users().get(serviceAccount.getId()).joinGroup(rootGroup.getId());
|
||||
realm.users().get(serviceAccountUserId).joinGroup(rootGroup.getId());
|
||||
ticket = authzClient.protection().permission().create(request).getTicket();
|
||||
response = authzClient.authorization(authzClient.obtainAccessToken().getToken()).authorize(new AuthorizationRequest(ticket));
|
||||
assertNotNull(response.getToken());
|
||||
|
||||
@@ -195,9 +195,9 @@ public class RolePolicyTest extends AbstractAuthzTest {
|
||||
ClientScopeRepresentation rolesScope = AdminApiUtil.findClientScopeByName(realm, OIDCLoginProtocolFactory.ROLES_SCOPE).toRepresentation();
|
||||
ClientResource clientResource = clients.get(client.getId());
|
||||
clientResource.removeDefaultClientScope(rolesScope.getId());
|
||||
UserRepresentation serviceAccountUser = clientResource.getServiceAccountUser();
|
||||
String serviceAccountUserId = clientResource.toRepresentation().getServiceAccountUserId();
|
||||
RoleRepresentation roleB = realm.roles().get("Role B").toRepresentation();
|
||||
realm.users().get(serviceAccountUser.getId()).roles().realmLevel().add(List.of(roleB));
|
||||
realm.users().get(serviceAccountUserId).roles().realmLevel().add(List.of(roleB));
|
||||
RolePolicyRepresentation roleRep = clientResource.authorization().policies().role().findByName("Role B Policy");
|
||||
roleRep.setFetchRoles(true);
|
||||
clientResource.authorization().policies().role().findById(roleRep.getId()).update(roleRep);
|
||||
|
||||
@@ -57,10 +57,10 @@ public class AuthorizationTest extends AbstractAuthorizationTest {
|
||||
enableAuthorizationServices(false);
|
||||
enableAuthorizationServices(true);
|
||||
|
||||
serviceAccount = clientResource.getServiceAccountUser();
|
||||
Assertions.assertNotNull(serviceAccount);
|
||||
String serviceAccountUserId = clientResource.toRepresentation().getServiceAccountUserId();
|
||||
Assertions.assertNotNull(serviceAccountUserId);
|
||||
realm = realmsResouce().realm(getRealmId());
|
||||
serviceAccountRoles = realm.users().get(serviceAccount.getId()).roles().clientLevel(resourceServer.getId()).listEffective();
|
||||
serviceAccountRoles = realm.users().get(serviceAccountUserId).roles().clientLevel(resourceServer.getId()).listEffective();
|
||||
Assertions.assertTrue(serviceAccountRoles.stream().anyMatch(roleRepresentation -> "uma_protection".equals(roleRepresentation.getName())));
|
||||
|
||||
RolePolicyRepresentation policy = new RolePolicyRepresentation();
|
||||
@@ -85,9 +85,9 @@ public class AuthorizationTest extends AbstractAuthorizationTest {
|
||||
policies = clientResource.authorization().policies().policies();
|
||||
assertTrue(policies.isEmpty());
|
||||
|
||||
serviceAccount = clientResource.getServiceAccountUser();
|
||||
Assertions.assertNotNull(serviceAccount);
|
||||
serviceAccountRoles = realm.users().get(serviceAccount.getId()).roles().clientLevel(resourceServer.getId()).listEffective();
|
||||
serviceAccountUserId = clientResource.toRepresentation().getServiceAccountUserId();
|
||||
Assertions.assertNotNull(serviceAccountUserId);
|
||||
serviceAccountRoles = realm.users().get(serviceAccountUserId).roles().clientLevel(resourceServer.getId()).listEffective();
|
||||
Assertions.assertTrue(serviceAccountRoles.stream().anyMatch(roleRepresentation -> "uma_protection".equals(roleRepresentation.getName())));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,7 +583,8 @@ public abstract class StandardTokenExchangeV2Test extends AbstractClientPolicies
|
||||
@Test
|
||||
@UncaughtServerErrorExpected
|
||||
public void testExchangeUsingServiceAccount() throws Exception {
|
||||
final UserRepresentation user = AdminApiUtil.findClientResourceByClientId(adminClient.realm(TEST), "subject-client").getServiceAccountUser();
|
||||
final String saUserId = AdminApiUtil.findClientResourceByClientId(adminClient.realm(TEST), "subject-client").toRepresentation().getServiceAccountUserId();
|
||||
final UserRepresentation user = adminClient.realm(TEST).users().get(saUserId).toRepresentation();
|
||||
oauth.realm(TEST);
|
||||
oauth.client("subject-client", "secret");
|
||||
|
||||
|
||||
@@ -153,8 +153,8 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest {
|
||||
|
||||
// Assign the role to TEST_CLIENT service account
|
||||
String testClientId = realm.clients().findByClientId(TEST_CLIENT).get(0).getId();
|
||||
UserRepresentation serviceAccount = realm.clients().get(testClientId).getServiceAccountUser();
|
||||
realm.users().get(serviceAccount.getId()).roles().clientLevel(resourceServerId).add(Collections.singletonList(role));
|
||||
String serviceAccountUserId = realm.clients().get(testClientId).toRepresentation().getServiceAccountUserId();
|
||||
realm.users().get(serviceAccountUserId).roles().clientLevel(resourceServerId).add(Collections.singletonList(role));
|
||||
|
||||
// Assign the role to test-user@localhost for user flows
|
||||
UserRepresentation testUser = realm.users().search("test-user@localhost").get(0);
|
||||
@@ -590,8 +590,8 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest {
|
||||
masterRealm.clients().create(transientClient);
|
||||
transientClient = masterRealm.clients().findByClientId(transientClient.getClientId()).get(0);
|
||||
|
||||
UserRepresentation userRep = masterRealm.clients().get(transientClient.getId()).getServiceAccountUser();
|
||||
masterRealm.users().get(userRep.getId()).roles().realmLevel().add(Collections.singletonList(masterRealm.roles().get(AdminRoles.ADMIN).toRepresentation()));
|
||||
String serviceAccountUserId = masterRealm.clients().get(transientClient.getId()).toRepresentation().getServiceAccountUserId();
|
||||
masterRealm.users().get(serviceAccountUserId).roles().realmLevel().add(Collections.singletonList(masterRealm.roles().get(AdminRoles.ADMIN).toRepresentation()));
|
||||
try {
|
||||
oauth.realm("master");
|
||||
oauth.client(transientClient.getClientId(), transientClient.getSecret());
|
||||
|
||||
@@ -204,7 +204,8 @@ public class ClientManager {
|
||||
}
|
||||
|
||||
public UserRepresentation getServiceAccountUser() {
|
||||
return clientResource.getServiceAccountUser();
|
||||
String saUserId = clientResource.toRepresentation().getServiceAccountUserId();
|
||||
return realm.users().get(saUserId).toRepresentation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user