Support unmanaged attributes for service accounts and make sure they are only managed through the admin api
Closes #29362 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
6dc28bc7b5
commit
b019cf6129
14 changed files with 115 additions and 278 deletions
|
@ -329,4 +329,9 @@ public interface UserResource {
|
||||||
@Path("impersonation")
|
@Path("impersonation")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
Map<String, Object> impersonate();
|
Map<String, Object> impersonate();
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("unmanagedAttributes")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
Map<String, List<String>> getUnmanagedAttributes();
|
||||||
}
|
}
|
||||||
|
|
|
@ -203,7 +203,7 @@ describe("User creation", () => {
|
||||||
|
|
||||||
cy.wait("@save-user").should(({ request, response }) => {
|
cy.wait("@save-user").should(({ request, response }) => {
|
||||||
expect(response?.statusCode).to.equal(204);
|
expect(response?.statusCode).to.equal(204);
|
||||||
expect(request.body.attributes, "response body").deep.equal({
|
expect(request.body.attributes, "response body").deep.contains({
|
||||||
"key-multiple": ["other value"],
|
"key-multiple": ["other value"],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
|
|
||||||
import { fetchAdminUI } from "../../context/auth/admin-ui-endpoint";
|
|
||||||
|
|
||||||
export const getUnmanagedAttributes = (
|
|
||||||
adminClient: KeycloakAdminClient,
|
|
||||||
id: string,
|
|
||||||
): Promise<Record<string, string[]> | undefined> =>
|
|
||||||
fetchAdminUI(adminClient, `ui-ext/users/${id}/unmanagedAttributes`);
|
|
|
@ -32,7 +32,6 @@ import {
|
||||||
RoutableTabs,
|
RoutableTabs,
|
||||||
useRoutableTab,
|
useRoutableTab,
|
||||||
} from "../components/routable-tabs/RoutableTabs";
|
} from "../components/routable-tabs/RoutableTabs";
|
||||||
import { getUnmanagedAttributes } from "../components/users/resource";
|
|
||||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||||
import { useAccess } from "../context/access/Access";
|
import { useAccess } from "../context/access/Access";
|
||||||
import { useRealm } from "../context/realm-context/RealmContext";
|
import { useRealm } from "../context/realm-context/RealmContext";
|
||||||
|
@ -117,7 +116,7 @@ export default function EditUser() {
|
||||||
userProfileMetadata: true,
|
userProfileMetadata: true,
|
||||||
}) as UIUserRepresentation | undefined,
|
}) as UIUserRepresentation | undefined,
|
||||||
adminClient.attackDetection.findOne({ id: id! }),
|
adminClient.attackDetection.findOne({ id: id! }),
|
||||||
getUnmanagedAttributes(adminClient, id!),
|
adminClient.users.getUnmanagedAttributes({ id: id! }),
|
||||||
adminClient.users.getProfile({ realm: realmName }),
|
adminClient.users.getProfile({ realm: realmName }),
|
||||||
]),
|
]),
|
||||||
([realm, userData, attackDetection, unmanagedAttributes, upConfig]) => {
|
([realm, userData, attackDetection, unmanagedAttributes, upConfig]) => {
|
||||||
|
|
|
@ -494,6 +494,15 @@ export class Users extends Resource<{ realm?: string }> {
|
||||||
urlParamKeys: ["id", "clientId"],
|
urlParamKeys: ["id", "clientId"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
public getUnmanagedAttributes = this.makeRequest<
|
||||||
|
{ id: string },
|
||||||
|
Record<string, string[]>
|
||||||
|
>({
|
||||||
|
method: "GET",
|
||||||
|
path: "/{id}/unmanagedAttributes",
|
||||||
|
urlParamKeys: ["id"],
|
||||||
|
});
|
||||||
|
|
||||||
constructor(client: KeycloakAdminClient) {
|
constructor(client: KeycloakAdminClient) {
|
||||||
super(client, {
|
super(client, {
|
||||||
path: "/admin/realms/{realm}/users",
|
path: "/admin/realms/{realm}/users",
|
||||||
|
|
|
@ -54,9 +54,4 @@ public final class AdminExtResource {
|
||||||
public UIRealmResource realm() {
|
public UIRealmResource realm() {
|
||||||
return new UIRealmResource(session, auth, adminEvent);
|
return new UIRealmResource(session, auth, adminEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path("/users")
|
|
||||||
public UsersResource users() {
|
|
||||||
return new UsersResource(session, auth);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
package org.keycloak.admin.ui.rest;
|
|
||||||
|
|
||||||
import static java.util.Collections.emptyList;
|
|
||||||
import static java.util.Optional.ofNullable;
|
|
||||||
import static org.keycloak.userprofile.UserProfileContext.USER_API;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Map.Entry;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import jakarta.ws.rs.GET;
|
|
||||||
import jakarta.ws.rs.Path;
|
|
||||||
import jakarta.ws.rs.Produces;
|
|
||||||
import jakarta.ws.rs.core.MediaType;
|
|
||||||
import org.jboss.resteasy.reactive.NoCache;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.RealmModel;
|
|
||||||
import org.keycloak.models.UserModel;
|
|
||||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
|
||||||
import org.keycloak.userprofile.UserProfile;
|
|
||||||
import org.keycloak.userprofile.UserProfileProvider;
|
|
||||||
import org.keycloak.utils.StringUtil;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
|
||||||
*/
|
|
||||||
public class UserResource {
|
|
||||||
|
|
||||||
private final KeycloakSession session;
|
|
||||||
private final UserModel user;
|
|
||||||
|
|
||||||
public UserResource(KeycloakSession session, UserModel user) {
|
|
||||||
this.session = session;
|
|
||||||
this.user = user;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("unmanagedAttributes")
|
|
||||||
@NoCache
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
public Map<String, List<String>> getUnmanagedAttributes() {
|
|
||||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
|
||||||
|
|
||||||
UserProfile profile = provider.create(USER_API, user);
|
|
||||||
Map<String, List<String>> managedAttributes = profile.getAttributes().getReadable();
|
|
||||||
Map<String, List<String>> attributes = new HashMap<>(user.getAttributes());
|
|
||||||
UPConfig upConfig = provider.getConfiguration();
|
|
||||||
|
|
||||||
if (upConfig.getUnmanagedAttributePolicy() == null) {
|
|
||||||
return Collections.emptyMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, List<String>> unmanagedAttributes = profile.getAttributes().getUnmanagedAttributes();
|
|
||||||
managedAttributes.entrySet().removeAll(unmanagedAttributes.entrySet());
|
|
||||||
attributes.entrySet().removeAll(managedAttributes.entrySet());
|
|
||||||
|
|
||||||
attributes.remove(UserModel.USERNAME);
|
|
||||||
attributes.remove(UserModel.EMAIL);
|
|
||||||
|
|
||||||
return attributes.entrySet().stream()
|
|
||||||
.filter(entry -> ofNullable(entry.getValue()).orElse(emptyList()).stream().anyMatch(StringUtil::isNotBlank))
|
|
||||||
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
package org.keycloak.admin.ui.rest;
|
|
||||||
|
|
||||||
import jakarta.ws.rs.NotFoundException;
|
|
||||||
import jakarta.ws.rs.Path;
|
|
||||||
import jakarta.ws.rs.PathParam;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.RealmModel;
|
|
||||||
import org.keycloak.models.UserModel;
|
|
||||||
import org.keycloak.models.UserSessionModel;
|
|
||||||
import org.keycloak.models.light.LightweightUserAdapter;
|
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
|
||||||
import jakarta.ws.rs.ForbiddenException;
|
|
||||||
|
|
||||||
public class UsersResource {
|
|
||||||
private final KeycloakSession session;
|
|
||||||
|
|
||||||
private final AdminPermissionEvaluator auth;
|
|
||||||
|
|
||||||
public UsersResource(KeycloakSession session, AdminPermissionEvaluator auth) {
|
|
||||||
this.session = session;
|
|
||||||
this.auth = auth;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Path("{id}")
|
|
||||||
public UserResource getUser(@PathParam("id") String id) {
|
|
||||||
RealmModel realm = session.getContext().getRealm();
|
|
||||||
UserModel user = null;
|
|
||||||
if (LightweightUserAdapter.isLightweightUser(id)) {
|
|
||||||
UserSessionModel userSession = session.sessions().getUserSession(realm, LightweightUserAdapter.getLightweightUserId(id));
|
|
||||||
if (userSession != null) {
|
|
||||||
user = userSession.getUser();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
user = session.users().getUserById(realm, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user == null) {
|
|
||||||
// we do this to make sure somebody can't phish ids
|
|
||||||
if (auth.users().canQuery()) throw new NotFoundException("User not found");
|
|
||||||
else throw new ForbiddenException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new UserResource(session, user);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -98,18 +98,6 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||||
return !isAllowEditUnmanagedAttribute();
|
return !isAllowEditUnmanagedAttribute();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (UserModel.USERNAME.equals(name)) {
|
|
||||||
if (isServiceAccountUser()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (UserModel.EMAIL.equals(name)) {
|
|
||||||
if (isServiceAccountUser()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isReadOnlyFromMetadata(name) || isReadOnlyInternalAttribute(name)) {
|
if (isReadOnlyFromMetadata(name) || isReadOnlyInternalAttribute(name)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -311,10 +299,6 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||||
return Collections.unmodifiableMap(this);
|
return Collections.unmodifiableMap(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean isServiceAccountUser() {
|
|
||||||
return user != null && user.getServiceAccountClientLink() != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private AttributeContext createAttributeContext(Entry<String, List<String>> attribute, AttributeMetadata metadata) {
|
private AttributeContext createAttributeContext(Entry<String, List<String>> attribute, AttributeMetadata metadata) {
|
||||||
return new AttributeContext(context, session, attribute, user, metadata, this);
|
return new AttributeContext(context, session, attribute, user, metadata, this);
|
||||||
}
|
}
|
||||||
|
@ -482,7 +466,7 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||||
return valuesStream.collect(Collectors.toList());
|
return valuesStream.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAllowUnmanagedAttribute() {
|
protected boolean isAllowUnmanagedAttribute() {
|
||||||
UnmanagedAttributePolicy unmanagedAttributePolicy = upConfig.getUnmanagedAttributePolicy();
|
UnmanagedAttributePolicy unmanagedAttributePolicy = upConfig.getUnmanagedAttributePolicy();
|
||||||
|
|
||||||
if (unmanagedAttributePolicy == null) {
|
if (unmanagedAttributePolicy == null) {
|
||||||
|
@ -501,11 +485,8 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||||
return UnmanagedAttributePolicy.ENABLED.equals(unmanagedAttributePolicy);
|
return UnmanagedAttributePolicy.ENABLED.equals(unmanagedAttributePolicy);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setUserName(Map<String, List<String>> newAttributes, List<String> lowerCaseEmailList) {
|
protected void setUserName(Map<String, List<String>> newAttributes, List<String> values) {
|
||||||
if (isServiceAccountUser()) {
|
newAttributes.put(UserModel.USERNAME, values);
|
||||||
return;
|
|
||||||
}
|
|
||||||
newAttributes.put(UserModel.USERNAME, lowerCaseEmailList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean isIncludeAttributeIfNotProvided(AttributeMetadata metadata) {
|
protected boolean isIncludeAttributeIfNotProvided(AttributeMetadata metadata) {
|
||||||
|
@ -530,10 +511,6 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isServiceAccountUser()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isReadOnlyInternalAttribute(name);
|
return isReadOnlyInternalAttribute(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -575,7 +552,7 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||||
return unmanagedAttributes;
|
return unmanagedAttributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AttributeMetadata createUnmanagedAttributeMetadata(String name) {
|
protected AttributeMetadata createUnmanagedAttributeMetadata(String name) {
|
||||||
return new AttributeMetadata(name, Integer.MAX_VALUE) {
|
return new AttributeMetadata(name, Integer.MAX_VALUE) {
|
||||||
final UnmanagedAttributePolicy unmanagedAttributePolicy = upConfig.getUnmanagedAttributePolicy();
|
final UnmanagedAttributePolicy unmanagedAttributePolicy = upConfig.getUnmanagedAttributePolicy();
|
||||||
|
|
||||||
|
|
|
@ -108,6 +108,7 @@ import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import jakarta.ws.rs.core.Response.Status;
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
import jakarta.ws.rs.core.UriBuilder;
|
import jakarta.ws.rs.core.UriBuilder;
|
||||||
|
import org.keycloak.utils.StringUtil;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
|
@ -118,6 +119,7 @@ import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -125,6 +127,8 @@ import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
|
import static java.util.Optional.ofNullable;
|
||||||
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
|
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
|
||||||
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
|
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
|
||||||
import static org.keycloak.userprofile.UserProfileContext.USER_API;
|
import static org.keycloak.userprofile.UserProfileContext.USER_API;
|
||||||
|
@ -1051,6 +1055,30 @@ public class UserResource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("unmanagedAttributes")
|
||||||
|
@NoCache
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Tag(name = KeycloakOpenAPI.Admin.Tags.USERS)
|
||||||
|
@Operation()
|
||||||
|
public Map<String, List<String>> getUnmanagedAttributes() {
|
||||||
|
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||||
|
|
||||||
|
UserProfile profile = provider.create(USER_API, user);
|
||||||
|
Map<String, List<String>> managedAttributes = profile.getAttributes().getReadable();
|
||||||
|
Map<String, List<String>> unmanagedAttributes = profile.getAttributes().getUnmanagedAttributes();
|
||||||
|
managedAttributes.entrySet().removeAll(unmanagedAttributes.entrySet());
|
||||||
|
Map<String, List<String>> attributes = new HashMap<>(user.getAttributes());
|
||||||
|
attributes.entrySet().removeAll(managedAttributes.entrySet());
|
||||||
|
|
||||||
|
attributes.remove(UserModel.USERNAME);
|
||||||
|
attributes.remove(UserModel.EMAIL);
|
||||||
|
|
||||||
|
return attributes.entrySet().stream()
|
||||||
|
.filter(entry -> ofNullable(entry.getValue()).orElse(emptyList()).stream().anyMatch(StringUtil::isNotBlank))
|
||||||
|
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the specified {@link UserSessionModel} into a {@link UserSessionRepresentation}.
|
* Converts the specified {@link UserSessionModel} into a {@link UserSessionRepresentation}.
|
||||||
*
|
*
|
||||||
|
|
|
@ -39,7 +39,6 @@ import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserProvider;
|
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.userprofile.config.DeclarativeUserProfileModel;
|
import org.keycloak.userprofile.config.DeclarativeUserProfileModel;
|
||||||
|
@ -107,7 +106,7 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
|
||||||
UserModel user, UserProfileMetadata metadata) {
|
UserModel user, UserProfileMetadata metadata) {
|
||||||
|
|
||||||
if (isServiceAccountUser(user)) {
|
if (isServiceAccountUser(user)) {
|
||||||
return new LegacyAttributes(context, attributes, user, metadata, session);
|
return new ServiceAccountAttributes(context, attributes, user, metadata, session);
|
||||||
}
|
}
|
||||||
return new DefaultAttributes(context, attributes, user, metadata, session);
|
return new DefaultAttributes(context, attributes, user, metadata, session);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,120 +0,0 @@
|
||||||
package org.keycloak.userprofile;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.keycloak.models.Constants;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.RealmModel;
|
|
||||||
import org.keycloak.models.UserModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables legacy support when managing attributes without the declarative provider.
|
|
||||||
*
|
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
|
||||||
*/
|
|
||||||
public class LegacyAttributes extends DefaultAttributes {
|
|
||||||
|
|
||||||
public LegacyAttributes(UserProfileContext context, Map<String, ?> attributes, UserModel user,
|
|
||||||
UserProfileMetadata profileMetadata, KeycloakSession session) {
|
|
||||||
super(context, attributes, user, profileMetadata, session);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isSupportedAttribute(String name) {
|
|
||||||
if (UserProfileContext.USER_API.equals(context) || UserProfileContext.ACCOUNT.equals(context)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (super.isSupportedAttribute(name)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.startsWith(Constants.USER_ATTRIBUTES_PREFIX)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isReadOnly(String name) {
|
|
||||||
RealmModel realm = session.getContext().getRealm();
|
|
||||||
|
|
||||||
if (isReadOnlyInternalAttribute(name)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (UserModel.USERNAME.equals(name)) {
|
|
||||||
if (isServiceAccountUser()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (UserProfileContext.IDP_REVIEW.equals(context)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (realm.isRegistrationEmailAsUsername()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return !realm.isEditUsernameAllowed();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (UserModel.EMAIL.equals(name)) {
|
|
||||||
if (isServiceAccountUser()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (UserProfileContext.IDP_REVIEW.equals(context)
|
|
||||||
|| UserProfileContext.USER_API.equals(context)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (realm.isRegistrationEmailAsUsername() && !realm.isEditUsernameAllowed()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Map<String, List<String>> getReadable() {
|
|
||||||
if(user == null || user.getAttributes() == null) {
|
|
||||||
return super.getReadable();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new HashMap<>(user.getAttributes());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Map<String, List<String>> getWritable() {
|
|
||||||
Map<String, List<String>> attributes = new HashMap<>(this);
|
|
||||||
RealmModel realm = session.getContext().getRealm();
|
|
||||||
|
|
||||||
for (String name : nameSet()) {
|
|
||||||
if (isReadOnly(name)) {
|
|
||||||
if (UserModel.USERNAME.equals(name)
|
|
||||||
&& realm.isRegistrationEmailAsUsername()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
attributes.remove(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return attributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isIncludeAttributeIfNotProvided(AttributeMetadata metadata) {
|
|
||||||
if (UserModel.LOCALE.equals(metadata.getName())) {
|
|
||||||
// locale is an internal attribute and should be updated as a regular attribute
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// user api expects that attributes are not updated if not provided when in legacy mode
|
|
||||||
return UserProfileContext.USER_API.equals(context);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
package org.keycloak.userprofile;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>A specific {@link Attributes} implementation to handle service accounts.
|
||||||
|
*
|
||||||
|
* <p>Service accounts are not regular users, and it should be possible to manage unmanaged attributes but only when
|
||||||
|
* operating through the {@link UserProfileContext#USER_API}. Otherwise, administrators will be forced to enable unmanaged
|
||||||
|
* attributes by setting a {@link org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy} or
|
||||||
|
* end up defining managed attributes that are specific for service accounts in the user profile configuration, which is
|
||||||
|
* mainly targeted for regular users.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class ServiceAccountAttributes extends DefaultAttributes {
|
||||||
|
|
||||||
|
public ServiceAccountAttributes(UserProfileContext context, Map<String, ?> attributes, UserModel user,
|
||||||
|
UserProfileMetadata profileMetadata, KeycloakSession session) {
|
||||||
|
super(context, attributes, user, profileMetadata, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReadOnly(String name) {
|
||||||
|
if (UserModel.USERNAME.equals(name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !UserProfileContext.USER_API.equals(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AttributeMetadata createUnmanagedAttributeMetadata(String name) {
|
||||||
|
return new AttributeMetadata(name, Integer.MAX_VALUE) {
|
||||||
|
@Override
|
||||||
|
public boolean canView(AttributeContext context) {
|
||||||
|
return UserProfileContext.USER_API.equals(context.getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canEdit(AttributeContext context) {
|
||||||
|
return UserProfileContext.USER_API.equals(context.getContext());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isAllowUnmanagedAttribute() {
|
||||||
|
return UserProfileContext.USER_API.equals(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setUserName(Map<String, List<String>> newAttributes, List<String> values) {
|
||||||
|
// can not update username for service accounts
|
||||||
|
}
|
||||||
|
}
|
|
@ -132,6 +132,7 @@ public class ServiceAccountUserProfileTest extends AbstractKeycloakTest {
|
||||||
UserRepresentation representation = serviceAccount.toRepresentation();
|
UserRepresentation representation = serviceAccount.toRepresentation();
|
||||||
String username = representation.getUsername();
|
String username = representation.getUsername();
|
||||||
|
|
||||||
|
assertNotNull(username);
|
||||||
assertNull(representation.getEmail());
|
assertNull(representation.getEmail());
|
||||||
|
|
||||||
serviceAccount.update(representation);
|
serviceAccount.update(representation);
|
||||||
|
@ -158,6 +159,10 @@ public class ServiceAccountUserProfileTest extends AbstractKeycloakTest {
|
||||||
representation = serviceAccount.toRepresentation();
|
representation = serviceAccount.toRepresentation();
|
||||||
assertFalse(representation.getAttributes().isEmpty());
|
assertFalse(representation.getAttributes().isEmpty());
|
||||||
assertEquals("attr-1-value", representation.getAttributes().get("attr-1").get(0));
|
assertEquals("attr-1-value", representation.getAttributes().get("attr-1").get(0));
|
||||||
|
|
||||||
|
Map<String, List<String>> unmanagedAttributes = test.users().get(userId).getUnmanagedAttributes();
|
||||||
|
|
||||||
|
assertEquals(1, unmanagedAttributes.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
Loading…
Reference in a new issue