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:
parent
26328a7c1e
commit
a668c2cb2b
4 changed files with 82 additions and 36 deletions
|
@ -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.",
|
||||||
|
|
|
@ -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,46 +238,70 @@ export const AdvancedSettings = ({ isOIDC, isSAML }: AdvancedSettingsProps) => {
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormGroup
|
<FormGroupField label="doNotStoreUsers">
|
||||||
className="pf-u-pb-3xl"
|
|
||||||
label={t("syncMode")}
|
|
||||||
labelIcon={
|
|
||||||
<HelpItem helpText={t("syncModeHelp")} fieldLabelId="syncMode" />
|
|
||||||
}
|
|
||||||
fieldId="syncMode"
|
|
||||||
>
|
|
||||||
<Controller
|
<Controller
|
||||||
name="config.syncMode"
|
name="config.doNotStoreUsers"
|
||||||
defaultValue={syncModes[0].toUpperCase()}
|
defaultValue="false"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Select
|
<Switch
|
||||||
toggleId="syncMode"
|
id="doNotStoreUsers"
|
||||||
required
|
label={t("on")}
|
||||||
direction="up"
|
labelOff={t("off")}
|
||||||
onToggle={() => setSyncModeOpen(!syncModeOpen)}
|
isChecked={field.value === "true"}
|
||||||
onSelect={(_, value) => {
|
onChange={(value) => {
|
||||||
field.onChange(value as string);
|
field.onChange(value.toString());
|
||||||
setSyncModeOpen(false);
|
// if field is checked, set sync mode to import
|
||||||
|
if (value) {
|
||||||
|
setValue("config.syncMode", "IMPORT");
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
selections={t(`syncModes.${field.value.toLowerCase()}`)}
|
/>
|
||||||
variant={SelectVariant.single}
|
|
||||||
aria-label={t("syncMode")}
|
|
||||||
isOpen={syncModeOpen}
|
|
||||||
>
|
|
||||||
{syncModes.map((option) => (
|
|
||||||
<SelectOption
|
|
||||||
selected={option === field.value}
|
|
||||||
key={option}
|
|
||||||
value={option.toUpperCase()}
|
|
||||||
>
|
|
||||||
{t(`syncModes.${option}`)}
|
|
||||||
</SelectOption>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroupField>
|
||||||
|
{syncModeAvailable && (
|
||||||
|
<FormGroup
|
||||||
|
className="pf-u-pb-3xl"
|
||||||
|
label={t("syncMode")}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem helpText={t("syncModeHelp")} fieldLabelId="syncMode" />
|
||||||
|
}
|
||||||
|
fieldId="syncMode"
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="config.syncMode"
|
||||||
|
defaultValue={syncModes[0].toUpperCase()}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
toggleId="syncMode"
|
||||||
|
required
|
||||||
|
direction="up"
|
||||||
|
onToggle={() => setSyncModeOpen(!syncModeOpen)}
|
||||||
|
onSelect={(_, value) => {
|
||||||
|
field.onChange(value as string);
|
||||||
|
setSyncModeOpen(false);
|
||||||
|
}}
|
||||||
|
selections={t(`syncModes.${field.value.toLowerCase()}`)}
|
||||||
|
variant={SelectVariant.single}
|
||||||
|
aria-label={t("syncMode")}
|
||||||
|
isOpen={syncModeOpen}
|
||||||
|
>
|
||||||
|
{syncModes.map((option) => (
|
||||||
|
<SelectOption
|
||||||
|
selected={option === field.value}
|
||||||
|
key={option}
|
||||||
|
value={option.toUpperCase()}
|
||||||
|
>
|
||||||
|
{t(`syncModes.${option}`)}
|
||||||
|
</SelectOption>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
session.users().setNotBeforeForUser(realm, user, Time.currentTime());
|
if (! LightweightUserAdapter.isLightweightUser(user)) {
|
||||||
|
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.
|
||||||
|
|
|
@ -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");
|
||||||
|
|
Loading…
Reference in a new issue