Support for transient brokering in admin console

Part-of: Add support for not importing brokered user into Keycloak database

Closes: #11334
This commit is contained in:
Hynek Mlnarik 2023-10-11 13:52:42 +02:00 committed by Hynek Mlnařík
parent 26328a7c1e
commit a668c2cb2b
4 changed files with 82 additions and 36 deletions

View file

@ -2871,6 +2871,7 @@
"disableUserInfo": "Disable user info", "disableUserInfo": "Disable user info",
"isAccessTokenJWT": "Access Token is JWT", "isAccessTokenJWT": "Access Token is JWT",
"userInfoUrl": "User Info URL", "userInfoUrl": "User Info URL",
"doNotStoreUsers": "Do not store users",
"issuer": "Issuer", "issuer": "Issuer",
"prompt": "Prompt", "prompt": "Prompt",
"prompts": { "prompts": {
@ -2954,6 +2955,7 @@
"disableUserInfoHelp": "Disable usage of User Info service to obtain additional user information? Default is to use this OIDC service.", "disableUserInfoHelp": "Disable usage of User Info service to obtain additional user information? Default is to use this OIDC service.",
"isAccessTokenJWTHelp": "The Access Token received from the Identity Provider is a JWT and its claims will be accessible for mappers.", "isAccessTokenJWTHelp": "The Access Token received from the Identity Provider is a JWT and its claims will be accessible for mappers.",
"userInfoUrlHelp": "The User Info Url. This is optional.", "userInfoUrlHelp": "The User Info Url. This is optional.",
"doNotStoreUsersHelp": "When enabled, users from this broker are not persisted in internal database.",
"issuerHelp": "The issuer identifier for the issuer of the response. If not provided, no validation will be performed.", "issuerHelp": "The issuer identifier for the issuer of the response. If not provided, no validation will be performed.",
"promptHelp": "Specifies whether the Authorization Server prompts the End-User for re-authentication and consent.", "promptHelp": "Specifies whether the Authorization Server prompts the End-User for re-authentication and consent.",
"acceptsPromptNoneHelp": "This is just used together with Identity Provider Authenticator or when kc_idp_hint points to this identity provider. In case that client sends a request with prompt=none and user is not yet authenticated, the error will not be directly returned to client, but the request with prompt=none will be forwarded to this identity provider.", "acceptsPromptNoneHelp": "This is just used together with Identity Provider Authenticator or when kc_idp_hint points to this identity provider. In case that client sends a request with prompt=none and user is not yet authenticated, the error will not be directly returned to client, but the request with prompt=none will be forwarded to this identity provider.",

View file

@ -96,6 +96,7 @@ export const AdvancedSettings = ({ isOIDC, isSAML }: AdvancedSettingsProps) => {
const { const {
control, control,
register, register,
setValue,
formState: { errors }, formState: { errors },
} = useFormContext<IdentityProviderRepresentation>(); } = useFormContext<IdentityProviderRepresentation>();
const [syncModeOpen, setSyncModeOpen] = useState(false); const [syncModeOpen, setSyncModeOpen] = useState(false);
@ -105,6 +106,12 @@ export const AdvancedSettings = ({ isOIDC, isSAML }: AdvancedSettingsProps) => {
defaultValue: "false", defaultValue: "false",
}); });
const claimFilterRequired = filteredByClaim === "true"; const claimFilterRequired = filteredByClaim === "true";
const transientSessions = useWatch({
control,
name: "config.doNotStoreUsers",
defaultValue: "false",
});
const syncModeAvailable = transientSessions === "false";
return ( return (
<> <>
{!isOIDC && !isSAML && ( {!isOIDC && !isSAML && (
@ -231,6 +238,29 @@ export const AdvancedSettings = ({ isOIDC, isSAML }: AdvancedSettingsProps) => {
defaultValue="" defaultValue=""
/> />
<FormGroupField label="doNotStoreUsers">
<Controller
name="config.doNotStoreUsers"
defaultValue="false"
control={control}
render={({ field }) => (
<Switch
id="doNotStoreUsers"
label={t("on")}
labelOff={t("off")}
isChecked={field.value === "true"}
onChange={(value) => {
field.onChange(value.toString());
// if field is checked, set sync mode to import
if (value) {
setValue("config.syncMode", "IMPORT");
}
}}
/>
)}
/>
</FormGroupField>
{syncModeAvailable && (
<FormGroup <FormGroup
className="pf-u-pb-3xl" className="pf-u-pb-3xl"
label={t("syncMode")} label={t("syncMode")}
@ -271,6 +301,7 @@ export const AdvancedSettings = ({ isOIDC, isSAML }: AdvancedSettingsProps) => {
)} )}
/> />
</FormGroup> </FormGroup>
)}
</> </>
); );
}; };

View file

@ -53,6 +53,7 @@ import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.UserManager; import org.keycloak.models.UserManager;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.light.LightweightUserAdapter;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.models.utils.RoleUtils; import org.keycloak.models.utils.RoleUtils;
@ -615,7 +616,9 @@ public class UserResource {
public void logout() { public void logout() {
auth.users().requireManage(user); auth.users().requireManage(user);
if (! LightweightUserAdapter.isLightweightUser(user)) {
session.users().setNotBeforeForUser(realm, user, Time.currentTime()); session.users().setNotBeforeForUser(realm, user, Time.currentTime());
}
session.sessions().getUserSessionsStream(realm, user) session.sessions().getUserSessionsStream(realm, user)
.collect(Collectors.toList()) // collect to avoid concurrent modification as backchannelLogout removes the user sessions. .collect(Collectors.toList()) // collect to avoid concurrent modification as backchannelLogout removes the user sessions.

View file

@ -34,6 +34,8 @@ import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException; import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.light.LightweightUserAdapter;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.policy.PasswordPolicyNotMetException; import org.keycloak.policy.PasswordPolicyNotMetException;
@ -68,6 +70,7 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.keycloak.models.Constants.SESSION_NOTE_LIGHTWEIGHT_USER;
import static org.keycloak.models.utils.KeycloakModelUtils.findGroupByPath; import static org.keycloak.models.utils.KeycloakModelUtils.findGroupByPath;
import static org.keycloak.userprofile.UserProfileContext.USER_API; import static org.keycloak.userprofile.UserProfileContext.USER_API;
@ -225,7 +228,14 @@ public class UsersResource {
*/ */
@Path("{id}") @Path("{id}")
public UserResource user(final @PathParam("id") String id) { public UserResource user(final @PathParam("id") String id) {
UserModel user = session.users().getUserById(realm, id); UserModel user = null;
if (LightweightUserAdapter.isLightweightUser(id)) {
UserSessionModel userSession = session.sessions().getUserSessionByBrokerSessionId(realm, LightweightUserAdapter.getLightweightUserId(id));
user = userSession.getUser();
} else {
user = session.users().getUserById(realm, id);
}
if (user == null) { if (user == null) {
// we do this to make sure somebody can't phish ids // we do this to make sure somebody can't phish ids
if (auth.users().canQuery()) throw new NotFoundException("User not found"); if (auth.users().canQuery()) throw new NotFoundException("User not found");