Initial security profile SPI to integrate default client policies
Closes #27189 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
c76cbc94d8
commit
41b706bb6a
33 changed files with 1274 additions and 277 deletions
|
@ -30,6 +30,7 @@ import org.keycloak.util.JsonSerialization;
|
|||
*/
|
||||
public class ClientPoliciesRepresentation {
|
||||
protected List<ClientPolicyRepresentation> policies = new ArrayList<>();
|
||||
private List<ClientPolicyRepresentation> globalPolicies;
|
||||
|
||||
public List<ClientPolicyRepresentation> getPolicies() {
|
||||
return policies;
|
||||
|
@ -39,6 +40,14 @@ public class ClientPoliciesRepresentation {
|
|||
this.policies = policies;
|
||||
}
|
||||
|
||||
public List<ClientPolicyRepresentation> getGlobalPolicies() {
|
||||
return globalPolicies;
|
||||
}
|
||||
|
||||
public void setGlobalPolicies(List<ClientPolicyRepresentation> globalPolicies) {
|
||||
this.globalPolicies = globalPolicies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return JsonSerialization.mapper.convertValue(this, JsonNode.class).hashCode();
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.representations.idm;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Default configuration for security profile. For the moment just a name and pointers
|
||||
* to default global client profiles and policies.
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class SecurityProfileConfiguration {
|
||||
|
||||
private String name;
|
||||
@JsonProperty("client-profiles")
|
||||
private String clientProfiles;
|
||||
@JsonProperty("client-policies")
|
||||
private String clientPolicies;
|
||||
@JsonIgnore
|
||||
private List<ClientProfileRepresentation> defaultClientProfiles;
|
||||
@JsonIgnore
|
||||
private List<ClientPolicyRepresentation> defaultClientPolicies;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getClientProfiles() {
|
||||
return clientProfiles;
|
||||
}
|
||||
|
||||
public void setClientProfiles(String clientProfiles) {
|
||||
this.clientProfiles = clientProfiles;
|
||||
}
|
||||
|
||||
public String getClientPolicies() {
|
||||
return clientPolicies;
|
||||
}
|
||||
|
||||
public void setClientPolicies(String clientPolicies) {
|
||||
this.clientPolicies = clientPolicies;
|
||||
}
|
||||
|
||||
public List<ClientProfileRepresentation> getDefaultClientProfiles() {
|
||||
return defaultClientProfiles;
|
||||
}
|
||||
|
||||
public void setDefaultClientProfiles(List<ClientProfileRepresentation> defaultClientProfiles) {
|
||||
this.defaultClientProfiles = defaultClientProfiles;
|
||||
}
|
||||
|
||||
public List<ClientPolicyRepresentation> getDefaultClientPolicies() {
|
||||
return defaultClientPolicies;
|
||||
}
|
||||
|
||||
public void setDefaultClientPolicies(List<ClientPolicyRepresentation> defaultClientPolicies) {
|
||||
this.defaultClientPolicies = defaultClientPolicies;
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import javax.ws.rs.Consumes;
|
|||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
|
||||
|
@ -17,6 +18,10 @@ public interface ClientPoliciesPoliciesResource {
|
|||
@Produces(MediaType.APPLICATION_JSON)
|
||||
ClientPoliciesRepresentation getPolicies();
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
ClientPoliciesRepresentation getPolicies(@QueryParam("include-global-policies") Boolean includeGlobalPolicies);
|
||||
|
||||
@PUT
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
void updatePolicies(final ClientPoliciesRepresentation clientPolicies);
|
||||
|
|
|
@ -1586,6 +1586,7 @@ times.minutes=Minutes
|
|||
disableUserInfo=Disable user info
|
||||
authorizationEncryptedResponseEnc=Authorization response encryption content encryption algorithm
|
||||
editCondition=Edit condition
|
||||
viewCondition=View condition
|
||||
ssoSessionMaxRememberMe=Max time before a session is expired when a user has set the remember me option. Tokens and browser sessions are invalidated when a session is expired. If not set it uses the standard SSO Session Max value.
|
||||
forcePostBinding=Force POST binding
|
||||
usersExplain=Users are the users in the current realm.
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
Flex,
|
||||
FlexItem,
|
||||
FormGroup,
|
||||
Label,
|
||||
PageSection,
|
||||
Text,
|
||||
TextVariants,
|
||||
|
@ -67,7 +68,12 @@ export default function NewClientPolicy() {
|
|||
const { t } = useTranslation();
|
||||
const { realm } = useRealm();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const [isGlobalPolicy, setIsGlobalPolicy] = useState(false);
|
||||
const [policies, setPolicies] = useState<ClientPolicyRepresentation[]>();
|
||||
const [globalPolicies, setGlobalPolicies] =
|
||||
useState<ClientPolicyRepresentation[]>();
|
||||
const [allPolicies, setAllPolicies] =
|
||||
useState<ClientPolicyRepresentation[]>();
|
||||
const [clientProfiles, setClientProfiles] = useState<
|
||||
ClientProfileRepresentation[]
|
||||
>([]);
|
||||
|
@ -101,7 +107,9 @@ export default function NewClientPolicy() {
|
|||
useFetch(
|
||||
async () => {
|
||||
const [policies, profiles] = await Promise.all([
|
||||
adminClient.clientPolicies.listPolicies(),
|
||||
adminClient.clientPolicies.listPolicies({
|
||||
includeGlobalPolicies: true,
|
||||
}),
|
||||
adminClient.clientPolicies.listProfiles({
|
||||
includeGlobalProfiles: true,
|
||||
}),
|
||||
|
@ -110,16 +118,29 @@ export default function NewClientPolicy() {
|
|||
return { policies, profiles };
|
||||
},
|
||||
({ policies, profiles }) => {
|
||||
const currentPolicy = policies.policies?.find(
|
||||
let currentPolicy = policies.policies?.find(
|
||||
(item) => item.name === policyName,
|
||||
);
|
||||
if (currentPolicy === undefined) {
|
||||
currentPolicy = policies.globalPolicies?.find(
|
||||
(item) => item.name === policyName,
|
||||
);
|
||||
setIsGlobalPolicy(currentPolicy !== undefined);
|
||||
}
|
||||
|
||||
const allClientProfiles = [
|
||||
...(profiles.globalProfiles ?? []),
|
||||
...(profiles.profiles ?? []),
|
||||
];
|
||||
|
||||
const allClientPolicies = [
|
||||
...(policies.globalPolicies ?? []),
|
||||
...(policies.policies ?? []),
|
||||
];
|
||||
|
||||
setPolicies(policies.policies ?? []);
|
||||
setGlobalPolicies(policies.globalPolicies ?? []);
|
||||
setAllPolicies(allClientPolicies);
|
||||
if (currentPolicy) {
|
||||
setupForm(currentPolicy);
|
||||
setClientProfiles(allClientProfiles);
|
||||
|
@ -134,7 +155,7 @@ export default function NewClientPolicy() {
|
|||
form.reset(policy);
|
||||
};
|
||||
|
||||
const policy = (policies || []).filter(
|
||||
const policy = (allPolicies || []).filter(
|
||||
(policy) => policy.name === policyName,
|
||||
);
|
||||
const policyConditions = policy[0]?.conditions || [];
|
||||
|
@ -151,8 +172,6 @@ export default function NewClientPolicy() {
|
|||
const createdForm = form.getValues();
|
||||
const createdPolicy = {
|
||||
...createdForm,
|
||||
profiles: [],
|
||||
conditions: [],
|
||||
};
|
||||
|
||||
const getAllPolicies = () => {
|
||||
|
@ -279,6 +298,7 @@ export default function NewClientPolicy() {
|
|||
policies: policies,
|
||||
});
|
||||
addAlert(t("deleteClientPolicyProfileSuccess"), AlertVariant.success);
|
||||
form.setValue("profiles", currentPolicy?.profiles || []);
|
||||
navigate(toEditClientPolicy({ realm, policyName: formValues.name! }));
|
||||
} catch (error) {
|
||||
addError(t("deleteClientPolicyProfileError"), error);
|
||||
|
@ -346,6 +366,10 @@ export default function NewClientPolicy() {
|
|||
policies: newPolicies,
|
||||
});
|
||||
setPolicies(newPolicies);
|
||||
const allClientPolicies = [...(globalPolicies || []), ...newPolicies];
|
||||
setAllPolicies(allClientPolicies);
|
||||
setCurrentPolicy(createdPolicy);
|
||||
form.setValue("profiles", createdPolicy.profiles);
|
||||
navigate(toEditClientPolicy({ realm, policyName: formValues.name! }));
|
||||
addAlert(t("addClientProfileSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
|
@ -393,9 +417,20 @@ export default function NewClientPolicy() {
|
|||
? policyName
|
||||
: "createPolicy"
|
||||
}
|
||||
badges={[
|
||||
{
|
||||
id: "global-client-policy-badge",
|
||||
text: isGlobalPolicy ? (
|
||||
<Label color="blue">{t("global")}</Label>
|
||||
) : (
|
||||
""
|
||||
),
|
||||
},
|
||||
]}
|
||||
divider
|
||||
dropdownItems={
|
||||
showAddConditionsAndProfilesForm || policyName
|
||||
(showAddConditionsAndProfilesForm || policyName) &&
|
||||
!isGlobalPolicy
|
||||
? [
|
||||
<DropdownItem
|
||||
key="delete"
|
||||
|
@ -410,6 +445,7 @@ export default function NewClientPolicy() {
|
|||
]
|
||||
: undefined
|
||||
}
|
||||
isReadOnly={isGlobalPolicy}
|
||||
isEnabled={field.value}
|
||||
onToggle={(value) => {
|
||||
if (!value) {
|
||||
|
@ -455,7 +491,7 @@ export default function NewClientPolicy() {
|
|||
variant="primary"
|
||||
type="submit"
|
||||
data-testid="saveCreatePolicy"
|
||||
isDisabled={!form.formState.isValid}
|
||||
isDisabled={!form.formState.isValid || isGlobalPolicy}
|
||||
>
|
||||
{t("save")}
|
||||
</Button>
|
||||
|
@ -463,7 +499,8 @@ export default function NewClientPolicy() {
|
|||
id="cancelCreatePolicy"
|
||||
variant="link"
|
||||
onClick={() =>
|
||||
showAddConditionsAndProfilesForm || policyName
|
||||
(showAddConditionsAndProfilesForm || policyName) &&
|
||||
!isGlobalPolicy
|
||||
? reset()
|
||||
: navigate(
|
||||
toClientPolicies({
|
||||
|
@ -474,7 +511,9 @@ export default function NewClientPolicy() {
|
|||
}
|
||||
data-testid="cancelCreatePolicy"
|
||||
>
|
||||
{showAddConditionsAndProfilesForm ? t("reload") : t("cancel")}
|
||||
{showAddConditionsAndProfilesForm && !isGlobalPolicy
|
||||
? t("reload")
|
||||
: t("cancel")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
{(showAddConditionsAndProfilesForm ||
|
||||
|
@ -490,26 +529,28 @@ export default function NewClientPolicy() {
|
|||
/>
|
||||
</Text>
|
||||
</FlexItem>
|
||||
<FlexItem align={{ default: "alignRight" }}>
|
||||
<Button
|
||||
id="addCondition"
|
||||
component={(props) => (
|
||||
<Link
|
||||
{...props}
|
||||
to={toNewClientPolicyCondition({
|
||||
realm,
|
||||
policyName: policyName!,
|
||||
})}
|
||||
></Link>
|
||||
)}
|
||||
variant="link"
|
||||
className="kc-addCondition"
|
||||
data-testid="addCondition"
|
||||
icon={<PlusCircleIcon />}
|
||||
>
|
||||
{t("addCondition")}
|
||||
</Button>
|
||||
</FlexItem>
|
||||
{!isGlobalPolicy && (
|
||||
<FlexItem align={{ default: "alignRight" }}>
|
||||
<Button
|
||||
id="addCondition"
|
||||
component={(props) => (
|
||||
<Link
|
||||
{...props}
|
||||
to={toNewClientPolicyCondition({
|
||||
realm,
|
||||
policyName: policyName!,
|
||||
})}
|
||||
></Link>
|
||||
)}
|
||||
variant="link"
|
||||
className="kc-addCondition"
|
||||
data-testid="addCondition"
|
||||
icon={<PlusCircleIcon />}
|
||||
>
|
||||
{t("addCondition")}
|
||||
</Button>
|
||||
</FlexItem>
|
||||
)}
|
||||
</Flex>
|
||||
{policyConditions.length > 0 ? (
|
||||
<DataList aria-label={t("conditions")} isCompact>
|
||||
|
@ -552,24 +593,26 @@ export default function NewClientPolicy() {
|
|||
helpText={type.helpText}
|
||||
fieldLabelId={condition.condition}
|
||||
/>
|
||||
<Button
|
||||
variant="link"
|
||||
aria-label="remove-condition"
|
||||
isInline
|
||||
icon={
|
||||
<TrashIcon
|
||||
className="kc-conditionType-trash-icon"
|
||||
data-testid={`delete-${condition.condition}-condition`}
|
||||
onClick={() => {
|
||||
toggleDeleteConditionDialog();
|
||||
setConditionToDelete({
|
||||
idx: idx,
|
||||
name: type.id!,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
></Button>
|
||||
{!isGlobalPolicy && (
|
||||
<Button
|
||||
variant="link"
|
||||
aria-label="remove-condition"
|
||||
isInline
|
||||
icon={
|
||||
<TrashIcon
|
||||
className="kc-conditionType-trash-icon"
|
||||
data-testid={`delete-${condition.condition}-condition`}
|
||||
onClick={() => {
|
||||
toggleDeleteConditionDialog();
|
||||
setConditionToDelete({
|
||||
idx: idx,
|
||||
name: type.id!,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
></Button>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
)}
|
||||
|
@ -609,18 +652,20 @@ export default function NewClientPolicy() {
|
|||
/>
|
||||
</Text>
|
||||
</FlexItem>
|
||||
<FlexItem align={{ default: "alignRight" }}>
|
||||
<Button
|
||||
id="addClientProfile"
|
||||
variant="link"
|
||||
className="kc-addClientProfile"
|
||||
data-testid="addClientProfile"
|
||||
icon={<PlusCircleIcon />}
|
||||
onClick={toggleModal}
|
||||
>
|
||||
{t("addClientProfile")}
|
||||
</Button>
|
||||
</FlexItem>
|
||||
{!isGlobalPolicy && (
|
||||
<FlexItem align={{ default: "alignRight" }}>
|
||||
<Button
|
||||
id="addClientProfile"
|
||||
variant="link"
|
||||
className="kc-addClientProfile"
|
||||
data-testid="addClientProfile"
|
||||
icon={<PlusCircleIcon />}
|
||||
onClick={toggleModal}
|
||||
>
|
||||
{t("addClientProfile")}
|
||||
</Button>
|
||||
</FlexItem>
|
||||
)}
|
||||
</Flex>
|
||||
{policyProfiles.length > 0 ? (
|
||||
<DataList aria-label={t("profiles")} isCompact>
|
||||
|
@ -663,24 +708,26 @@ export default function NewClientPolicy() {
|
|||
}
|
||||
fieldLabelId={profile}
|
||||
/>
|
||||
<Button
|
||||
variant="link"
|
||||
aria-label="remove-client-profile"
|
||||
isInline
|
||||
icon={
|
||||
<TrashIcon
|
||||
className="kc-conditionType-trash-icon"
|
||||
data-testid="deleteClientProfileDropdown"
|
||||
onClick={() => {
|
||||
toggleDeleteProfileDialog();
|
||||
setProfileToDelete({
|
||||
idx: idx,
|
||||
name: type!,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
></Button>
|
||||
{!isGlobalPolicy && (
|
||||
<Button
|
||||
variant="link"
|
||||
aria-label="remove-client-profile"
|
||||
isInline
|
||||
icon={
|
||||
<TrashIcon
|
||||
className="kc-conditionType-trash-icon"
|
||||
data-testid="deleteClientProfileDropdown"
|
||||
onClick={() => {
|
||||
toggleDeleteProfileDialog();
|
||||
setProfileToDelete({
|
||||
idx: idx,
|
||||
name: type!,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
></Button>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</DataListCell>,
|
||||
|
|
|
@ -18,12 +18,13 @@ import { camelCase } from "lodash-es";
|
|||
import { useState } from "react";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { FormPanel, HelpItem } from "ui-shared";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { HelpItem } from "ui-shared";
|
||||
import { adminClient } from "../admin-client";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
import { DynamicComponents } from "../components/dynamic/DynamicComponents";
|
||||
import { FormAccess } from "../components/form/FormAccess";
|
||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
||||
import { useFetch } from "../utils/useFetch";
|
||||
|
@ -44,6 +45,7 @@ export default function NewClientPolicyCondition() {
|
|||
const { realm } = useRealm();
|
||||
|
||||
const [openConditionType, setOpenConditionType] = useState(false);
|
||||
const [isGlobalPolicy, setIsGlobalPolicy] = useState(false);
|
||||
const [policies, setPolicies] = useState<ClientPolicyRepresentation[]>([]);
|
||||
|
||||
const [condition, setCondition] = useState<
|
||||
|
@ -72,15 +74,24 @@ export default function NewClientPolicyCondition() {
|
|||
};
|
||||
|
||||
useFetch(
|
||||
() => adminClient.clientPolicies.listPolicies(),
|
||||
() =>
|
||||
adminClient.clientPolicies.listPolicies({
|
||||
includeGlobalPolicies: true,
|
||||
}),
|
||||
|
||||
(policies) => {
|
||||
setPolicies(policies.policies ?? []);
|
||||
|
||||
if (conditionName) {
|
||||
const currentPolicy = policies.policies?.find(
|
||||
let currentPolicy = policies.policies?.find(
|
||||
(item) => item.name === policyName,
|
||||
);
|
||||
if (currentPolicy === undefined) {
|
||||
currentPolicy = policies.globalPolicies?.find(
|
||||
(item) => item.name === policyName,
|
||||
);
|
||||
setIsGlobalPolicy(currentPolicy !== undefined);
|
||||
}
|
||||
|
||||
const typeAndConfigData = currentPolicy?.conditions?.find(
|
||||
(item) => item.condition === conditionName,
|
||||
|
@ -170,14 +181,22 @@ export default function NewClientPolicyCondition() {
|
|||
};
|
||||
|
||||
return (
|
||||
<PageSection variant="light">
|
||||
<FormPanel
|
||||
className="kc-login-screen"
|
||||
title={conditionName ? t("editCondition") : t("addCondition")}
|
||||
>
|
||||
<>
|
||||
<ViewHeader
|
||||
titleKey={
|
||||
conditionName
|
||||
? isGlobalPolicy
|
||||
? t("viewCondition")
|
||||
: t("editCondition")
|
||||
: t("addCondition")
|
||||
}
|
||||
divider
|
||||
/>
|
||||
<PageSection variant="light">
|
||||
<FormAccess
|
||||
isHorizontal
|
||||
role="manage-realm"
|
||||
isReadOnly={isGlobalPolicy}
|
||||
className="pf-v5-u-mt-lg"
|
||||
onSubmit={form.handleSubmit(save)}
|
||||
>
|
||||
|
@ -245,27 +264,48 @@ export default function NewClientPolicyCondition() {
|
|||
<FormProvider {...form}>
|
||||
<DynamicComponents properties={conditionProperties} />
|
||||
</FormProvider>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
data-testid="addCondition-saveBtn"
|
||||
isDisabled={conditionType === "" && !conditionName}
|
||||
>
|
||||
{conditionName ? t("save") : t("add")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
data-testid="addCondition-cancelBtn"
|
||||
onClick={() =>
|
||||
navigate(toEditClientPolicy({ realm, policyName: policyName! }))
|
||||
}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
{!isGlobalPolicy && (
|
||||
<ActionGroup>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
data-testid="addCondition-saveBtn"
|
||||
isDisabled={
|
||||
conditionType === "" && !conditionName && isGlobalPolicy
|
||||
}
|
||||
>
|
||||
{conditionName ? t("save") : t("add")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
data-testid="addCondition-cancelBtn"
|
||||
onClick={() =>
|
||||
navigate(
|
||||
toEditClientPolicy({ realm, policyName: policyName! }),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
)}
|
||||
</FormAccess>
|
||||
</FormPanel>
|
||||
</PageSection>
|
||||
{isGlobalPolicy && (
|
||||
<div className="kc-backToProfile">
|
||||
<Button
|
||||
component={(props) => (
|
||||
<Link
|
||||
{...props}
|
||||
to={toEditClientPolicy({ realm, policyName: policyName! })}
|
||||
/>
|
||||
)}
|
||||
variant="primary"
|
||||
>
|
||||
{t("back")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
Divider,
|
||||
Flex,
|
||||
FlexItem,
|
||||
Label,
|
||||
PageSection,
|
||||
Radio,
|
||||
Switch,
|
||||
|
@ -36,29 +37,48 @@ import { toEditClientPolicy } from "./routes/EditClientPolicy";
|
|||
|
||||
import "./realm-settings-section.css";
|
||||
|
||||
type ClientPolicy = ClientPolicyRepresentation & {
|
||||
global?: boolean;
|
||||
};
|
||||
|
||||
export const PoliciesTab = () => {
|
||||
const { t } = useTranslation();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const { realm } = useRealm();
|
||||
const navigate = useNavigate();
|
||||
const [show, setShow] = useState(false);
|
||||
const [policies, setPolicies] = useState<ClientPolicyRepresentation[]>();
|
||||
const [selectedPolicy, setSelectedPolicy] =
|
||||
useState<ClientPolicyRepresentation>();
|
||||
const [policies, setPolicies] = useState<ClientPolicy[]>();
|
||||
const [selectedPolicy, setSelectedPolicy] = useState<ClientPolicy>();
|
||||
const [key, setKey] = useState(0);
|
||||
const [code, setCode] = useState<string>();
|
||||
const [tablePolicies, setTablePolicies] =
|
||||
useState<ClientPolicyRepresentation[]>();
|
||||
const [tablePolicies, setTablePolicies] = useState<ClientPolicy[]>();
|
||||
const refresh = () => setKey(key + 1);
|
||||
|
||||
const form = useForm<Record<string, boolean>>({ mode: "onChange" });
|
||||
|
||||
useFetch(
|
||||
() => adminClient.clientPolicies.listPolicies(),
|
||||
(policies) => {
|
||||
setPolicies(policies.policies),
|
||||
setTablePolicies(policies.policies || []),
|
||||
setCode(prettyPrintJSON(policies.policies));
|
||||
() =>
|
||||
adminClient.clientPolicies.listPolicies({
|
||||
includeGlobalPolicies: true,
|
||||
}),
|
||||
(allPolicies) => {
|
||||
const globalPolicies = allPolicies.globalPolicies?.map(
|
||||
(globalPolicies) => ({
|
||||
...globalPolicies,
|
||||
global: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const policies = allPolicies.policies?.map((policies) => ({
|
||||
...policies,
|
||||
global: false,
|
||||
}));
|
||||
|
||||
const allClientPolicies = globalPolicies?.concat(policies ?? []);
|
||||
|
||||
setPolicies(allClientPolicies),
|
||||
setTablePolicies(allClientPolicies || []),
|
||||
setCode(prettyPrintJSON(allClientPolicies));
|
||||
},
|
||||
[key],
|
||||
);
|
||||
|
@ -68,16 +88,19 @@ export const PoliciesTab = () => {
|
|||
const saveStatus = async () => {
|
||||
const switchValues = form.getValues();
|
||||
|
||||
const updatedPolicies = policies?.map<ClientPolicyRepresentation>(
|
||||
(policy) => {
|
||||
const updatedPolicies = policies
|
||||
?.filter((policy) => {
|
||||
return !policy.global;
|
||||
})
|
||||
.map<ClientPolicyRepresentation>((policy) => {
|
||||
const enabled = switchValues[policy.name!];
|
||||
|
||||
return {
|
||||
const enabledPolicy = {
|
||||
...policy,
|
||||
enabled,
|
||||
};
|
||||
},
|
||||
);
|
||||
delete enabledPolicy.global;
|
||||
return enabledPolicy;
|
||||
});
|
||||
|
||||
try {
|
||||
await adminClient.clientPolicies.updatePolicy({
|
||||
|
@ -90,15 +113,13 @@ export const PoliciesTab = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const ClientPolicyDetailLink = ({ name }: ClientPolicyRepresentation) => (
|
||||
<Link to={toEditClientPolicy({ realm, policyName: name! })}>{name}</Link>
|
||||
const ClientPolicyDetailLink = (row: ClientPolicy) => (
|
||||
<Link to={toEditClientPolicy({ realm, policyName: row.name! })}>
|
||||
{row.name} {row.global && <Label color="blue">{t("global")}</Label>}
|
||||
</Link>
|
||||
);
|
||||
|
||||
const SwitchRenderer = ({
|
||||
clientPolicy,
|
||||
}: {
|
||||
clientPolicy: ClientPolicyRepresentation;
|
||||
}) => {
|
||||
const SwitchRenderer = ({ clientPolicy }: { clientPolicy: ClientPolicy }) => {
|
||||
const [toggleDisableDialog, DisableConfirm] = useConfirmDialog({
|
||||
titleKey: "disablePolicyConfirmTitle",
|
||||
messageKey: "disablePolicyConfirm",
|
||||
|
@ -122,6 +143,7 @@ export const PoliciesTab = () => {
|
|||
label={t("enabled")}
|
||||
labelOff={t("disabled")}
|
||||
isChecked={field.value}
|
||||
isDisabled={clientPolicy.global}
|
||||
onChange={(_event, value) => {
|
||||
if (!value) {
|
||||
toggleDisableDialog();
|
||||
|
@ -144,7 +166,7 @@ export const PoliciesTab = () => {
|
|||
}
|
||||
|
||||
try {
|
||||
const obj: ClientPolicyRepresentation[] = JSON.parse(code);
|
||||
const obj: ClientPolicy[] = JSON.parse(code);
|
||||
|
||||
try {
|
||||
await adminClient.clientPolicies.updatePolicy({
|
||||
|
@ -169,9 +191,15 @@ export const PoliciesTab = () => {
|
|||
continueButtonLabel: t("delete"),
|
||||
continueButtonVariant: ButtonVariant.danger,
|
||||
onConfirm: async () => {
|
||||
const updatedPolicies = policies?.filter(
|
||||
(policy) => policy.name !== selectedPolicy?.name,
|
||||
);
|
||||
const updatedPolicies = policies
|
||||
?.filter((policy) => {
|
||||
return !policy.global && policy.name !== selectedPolicy?.name;
|
||||
})
|
||||
.map<ClientPolicyRepresentation>((policy) => {
|
||||
const newPolicy = { ...policy };
|
||||
delete newPolicy.global;
|
||||
return newPolicy;
|
||||
});
|
||||
|
||||
try {
|
||||
await adminClient.clientPolicies.updatePolicy({
|
||||
|
@ -250,6 +278,7 @@ export const PoliciesTab = () => {
|
|||
</Button>
|
||||
</ToolbarItem>
|
||||
}
|
||||
isRowDisabled={(value) => !!value.global}
|
||||
actions={[
|
||||
{
|
||||
title: t("delete"),
|
||||
|
@ -257,7 +286,7 @@ export const PoliciesTab = () => {
|
|||
toggleDeleteDialog();
|
||||
setSelectedPolicy(item);
|
||||
},
|
||||
} as Action<ClientPolicyRepresentation>,
|
||||
} as Action<ClientPolicy>,
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
|
|
|
@ -4,5 +4,6 @@ import type ClientPolicyRepresentation from "./clientPolicyRepresentation.js";
|
|||
* https://www.keycloak.org/docs-api/15.0/rest-api/#_clientpoliciesrepresentation
|
||||
*/
|
||||
export default interface ClientPoliciesRepresentation {
|
||||
globalPolicies?: ClientPolicyRepresentation[];
|
||||
policies?: ClientPolicyRepresentation[];
|
||||
}
|
||||
|
|
|
@ -38,9 +38,16 @@ export class ClientPolicies extends Resource<{ realm?: string }> {
|
|||
|
||||
/* Client Policies */
|
||||
|
||||
public listPolicies = this.makeRequest<{}, ClientPoliciesRepresentation>({
|
||||
public listPolicies = this.makeRequest<
|
||||
{ includeGlobalPolicies?: boolean },
|
||||
ClientPoliciesRepresentation
|
||||
>({
|
||||
method: "GET",
|
||||
path: "/policies",
|
||||
queryParamKeys: ["include-global-policies"],
|
||||
keyTransform: {
|
||||
includeGlobalPolicies: "include-global-policies",
|
||||
},
|
||||
});
|
||||
|
||||
public updatePolicy = this.makeRequest<ClientPoliciesRepresentation, void>({
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.securityprofile;
|
||||
|
||||
import java.util.List;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.representations.idm.ClientPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.ClientProfileRepresentation;
|
||||
|
||||
/**
|
||||
* The security profile provider is a default security configuration that enforces a
|
||||
* minimum level of security in the keycloak environment. For the moment the class
|
||||
* is just used for client policies but it can be extended for password policies
|
||||
* or any other security configuration in the future.
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public interface SecurityProfileProvider extends Provider {
|
||||
|
||||
/**
|
||||
* Name of the security profile.
|
||||
* @return The name
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* List of default client profiles that the security profile contains.
|
||||
* @return The list of client profiles defined
|
||||
*/
|
||||
List<ClientProfileRepresentation> getDefaultClientProfiles();
|
||||
|
||||
/**
|
||||
* List of default client policies defined in the security profile.
|
||||
* @return The list of client policies defined
|
||||
*/
|
||||
List<ClientPolicyRepresentation> getDefaultClientPolicies();
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.securityprofile;
|
||||
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public interface SecurityProfileProviderFactory extends ProviderFactory<SecurityProfileProvider>{
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.securityprofile;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class SecurityProfileSpi implements Spi {
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "security-profile";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return SecurityProfileProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return SecurityProfileProviderFactory.class;
|
||||
}
|
||||
}
|
|
@ -99,3 +99,4 @@ org.keycloak.device.DeviceRepresentationSpi
|
|||
org.keycloak.health.LoadBalancerCheckSpi
|
||||
org.keycloak.cookie.CookieSpi
|
||||
org.keycloak.organization.OrganizationSpi
|
||||
org.keycloak.securityprofile.SecurityProfileSpi
|
||||
|
|
|
@ -93,9 +93,10 @@ public interface ClientPolicyManager extends Provider {
|
|||
* when getting client policies via Admin REST API, returns the existing client policies set on the realm.
|
||||
*
|
||||
* @param realm - the realm whose client policies is to be returned
|
||||
* @param includeGlobalPolicies - the json representation will include the default policies
|
||||
* @return the json representation of the client policies set on the realm
|
||||
*/
|
||||
ClientPoliciesRepresentation getClientPolicies(RealmModel realm) throws ClientPolicyException;
|
||||
ClientPoliciesRepresentation getClientPolicies(RealmModel realm, boolean includeGlobalPolicies) throws ClientPolicyException;
|
||||
|
||||
/**
|
||||
* when exporting realm, or retrieve the realm for admin REST API, prepares the exported representation of the client profiles and policies.
|
||||
|
|
|
@ -18,8 +18,13 @@
|
|||
|
||||
package org.keycloak.services.clientpolicy;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
|
@ -27,10 +32,7 @@ import java.util.List;
|
|||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.component.JsonConfigComponentModel;
|
||||
|
@ -38,14 +40,15 @@ import org.keycloak.models.Constants;
|
|||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
|
||||
import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepresentation;
|
||||
import org.keycloak.representations.idm.ClientPolicyConditionRepresentation;
|
||||
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
|
||||
import org.keycloak.representations.idm.ClientPolicyExecutorRepresentation;
|
||||
import org.keycloak.representations.idm.ClientPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.ClientProfileRepresentation;
|
||||
import org.keycloak.representations.idm.ClientProfilesRepresentation;
|
||||
import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepresentation;
|
||||
import org.keycloak.securityprofile.SecurityProfileProvider;
|
||||
import org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProvider;
|
||||
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
|
||||
import org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
|
@ -58,6 +61,43 @@ public class ClientPoliciesUtil {
|
|||
|
||||
private static final Logger logger = Logger.getLogger(ClientPoliciesUtil.class);
|
||||
|
||||
public static InputStream getJsonFileFromClasspathOrConfFolder(String name) throws IOException {
|
||||
final String fileName = name + ".json";
|
||||
// first try to read the json configuration file from classpath
|
||||
InputStream is = ClientPoliciesUtil.class.getResourceAsStream("/" + fileName);
|
||||
if (is == null) {
|
||||
Path path = Paths.get(System.getProperty("jboss.server.config.dir")).resolve(fileName);
|
||||
if (!Files.isReadable(path)) {
|
||||
throw new IOException(String.format("File \"%s\" does not exists under the config folder", path));
|
||||
}
|
||||
is = Files.newInputStream(path);
|
||||
}
|
||||
return is;
|
||||
}
|
||||
|
||||
public static List<ClientProfileRepresentation> readGlobalClientProfilesRepresentation(KeycloakSession session, String name) throws ClientPolicyException {
|
||||
if (name == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
try (InputStream is = getJsonFileFromClasspathOrConfFolder(name)) {
|
||||
return getValidatedGlobalClientProfilesRepresentation(session, is);
|
||||
} catch (IOException e) {
|
||||
throw new ClientPolicyException("Error reading profiles from " + name, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<ClientPolicyRepresentation> readGlobalClientPoliciesRepresentation(KeycloakSession session, String name,
|
||||
List<ClientProfileRepresentation> profiles) throws ClientPolicyException {
|
||||
if (name == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
try (InputStream is = getJsonFileFromClasspathOrConfFolder(name)) {
|
||||
return getValidatedGlobalClientPoliciesRepresentation(session, is, profiles);
|
||||
} catch (IOException e) {
|
||||
throw new ClientPolicyException("Error reading profiles from " + name, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* gets existing client profiles in a realm as representation.
|
||||
* not return null.
|
||||
|
@ -183,6 +223,24 @@ public class ClientPoliciesUtil {
|
|||
return updatingProfileList;
|
||||
}
|
||||
|
||||
/**
|
||||
* get validated and modified global (built-in) client policies set on keycloak app as representation.
|
||||
* it is loaded from json file enclosed in keycloak's binary.
|
||||
* not return null.
|
||||
*/
|
||||
static List<ClientPolicyRepresentation> getValidatedGlobalClientPoliciesRepresentation(KeycloakSession session, InputStream is, List<ClientProfileRepresentation> profiles) throws ClientPolicyException {
|
||||
ClientPoliciesRepresentation proposedPoliciesRep = null;
|
||||
try {
|
||||
proposedPoliciesRep = JsonSerialization.readValue(is, ClientPoliciesRepresentation.class);
|
||||
} catch (Exception e) {
|
||||
throw new ClientPolicyException("failed to deserialize global proposed client profiles json string.", e.getMessage());
|
||||
}
|
||||
if (proposedPoliciesRep == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return validatePolicies(session, proposedPoliciesRep.getPolicies(), profiles, Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* convert client profiles as representation to json.
|
||||
* can return null.
|
||||
|
@ -240,7 +298,7 @@ public class ClientPoliciesUtil {
|
|||
Set<String> globalProfileNames = globalClientProfiles.stream().map(ClientProfileRepresentation::getName).collect(Collectors.toSet());
|
||||
for (ClientProfileRepresentation clientProfile : proposedProfileRepList) {
|
||||
if (globalProfileNames.contains(clientProfile.getName())) {
|
||||
throw new ClientPolicyException("Proposed profile name duplicated as the name of some global profile");
|
||||
throw new ClientPolicyException("Proposed profile name '" + clientProfile.getName() + "' is duplicated as a global profile");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -298,32 +356,42 @@ public class ClientPoliciesUtil {
|
|||
return convertClientPoliciesJsonToRepresentation(policiesJson);
|
||||
}
|
||||
|
||||
static List<ClientProfileRepresentation> getGlobalClientProfiles(KeycloakSession session) {
|
||||
SecurityProfileProvider securityProfile = session.getProvider(SecurityProfileProvider.class);
|
||||
return securityProfile.getDefaultClientProfiles();
|
||||
}
|
||||
|
||||
static List<ClientPolicyRepresentation> getGlobalClientPolicies(KeycloakSession session) {
|
||||
SecurityProfileProvider securityProfile = session.getProvider(SecurityProfileProvider.class);
|
||||
return securityProfile.getDefaultClientPolicies();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets existing enabled client policies in a realm.
|
||||
* not return null.
|
||||
*/
|
||||
static List<ClientPolicy> getEnabledClientPolicies(KeycloakSession session, RealmModel realm) {
|
||||
// get the global policies defined in the security profile
|
||||
List<ClientPolicyRepresentation> policiesRep = new ArrayList<>(getGlobalClientPolicies(session));
|
||||
|
||||
// get existing profiles as json
|
||||
String policiesJson = getClientPoliciesJsonString(realm);
|
||||
if (policiesJson == null) {
|
||||
return Collections.emptyList();
|
||||
if (policiesJson != null) {
|
||||
// deserialize existing policies (json -> representation)
|
||||
try {
|
||||
policiesRep.addAll(convertClientPoliciesJsonToRepresentation(policiesJson).getPolicies());
|
||||
} catch (ClientPolicyException e) {
|
||||
logger.warnv("Failed to serialize client policies json string. err={0}, errDetail={1}", e.getError(), e.getErrorDetail());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
// deserialize existing policies (json -> representation)
|
||||
ClientPoliciesRepresentation policiesRep = null;
|
||||
try {
|
||||
policiesRep = convertClientPoliciesJsonToRepresentation(policiesJson);
|
||||
} catch (ClientPolicyException e) {
|
||||
logger.warnv("Failed to serialize client policies json string. err={0}, errDetail={1}", e.getError(), e.getErrorDetail());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (policiesRep == null || policiesRep.getPolicies() == null) {
|
||||
if (policiesRep.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// constructing existing policies (representation -> model)
|
||||
List<ClientPolicy> policyList = new ArrayList<>();
|
||||
for (ClientPolicyRepresentation policyRep: policiesRep.getPolicies()) {
|
||||
for (ClientPolicyRepresentation policyRep: policiesRep) {
|
||||
// ignore policy without name
|
||||
if (policyRep.getName() == null) {
|
||||
logger.warnf("Ignored client policy without name in the realm %s", realm.getName());
|
||||
|
@ -395,6 +463,80 @@ public class ClientPoliciesUtil {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the policies passed with the profiles.
|
||||
* @param session The session
|
||||
* @param proposedPoliciesRepList The policies to validate
|
||||
* @param profiles The already validated profiles used by the policies
|
||||
* @param globalPolicies The global policies defined already validated
|
||||
* @return The validated policies
|
||||
* @throws ClientPolicyException Some error in the policies
|
||||
*/
|
||||
static private List<ClientPolicyRepresentation> validatePolicies(KeycloakSession session, List<ClientPolicyRepresentation> proposedPoliciesRepList,
|
||||
List<ClientProfileRepresentation> profiles, List<ClientPolicyRepresentation> globalPolicies) throws ClientPolicyException {
|
||||
|
||||
// empty policies is valid
|
||||
if (proposedPoliciesRepList == null || proposedPoliciesRepList.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// check for duplicated names
|
||||
if (proposedPoliciesRepList.size() != proposedPoliciesRepList.stream().map(i->i.getName()).distinct().count()) {
|
||||
throw new ClientPolicyException("proposed client policy name duplicated");
|
||||
}
|
||||
|
||||
// Conflict with any global policy is not allowed
|
||||
Set<String> globalPolicyNames = globalPolicies.stream().map(ClientPolicyRepresentation::getName).collect(Collectors.toSet());
|
||||
for (ClientPolicyRepresentation clientPolicy : proposedPoliciesRepList) {
|
||||
if (globalPolicyNames.contains(clientPolicy.getName())) {
|
||||
throw new ClientPolicyException("Proposed policy name '" + clientPolicy.getName() + "' is duplicated as a global policy");
|
||||
}
|
||||
}
|
||||
|
||||
// construct validated and modified profiles from builtin profiles in JSON file enclosed in keycloak binary.
|
||||
List<ClientPolicyRepresentation> updatingPolicyList = new LinkedList<>();
|
||||
|
||||
Set<String> profileNames = profiles.stream().map(ClientProfileRepresentation::getName).collect(Collectors.toSet());
|
||||
|
||||
for (ClientPolicyRepresentation proposedPolicyRep : proposedPoliciesRepList) {
|
||||
if (proposedPolicyRep.getName() == null) {
|
||||
throw new ClientPolicyException("client policy without name not allowed");
|
||||
}
|
||||
|
||||
ClientPolicyRepresentation policyRep = new ClientPolicyRepresentation();
|
||||
policyRep.setName(proposedPolicyRep.getName());
|
||||
policyRep.setDescription(proposedPolicyRep.getDescription());
|
||||
policyRep.setEnabled(proposedPolicyRep.isEnabled() != null ? proposedPolicyRep.isEnabled() : Boolean.FALSE);
|
||||
|
||||
policyRep.setConditions(new ArrayList<>());
|
||||
if (proposedPolicyRep.getConditions() != null) {
|
||||
for (ClientPolicyConditionRepresentation condition : proposedPolicyRep.getConditions()) {
|
||||
if (Profile.isFeatureEnabled(Profile.Feature.CLIENT_POLICIES) && !isValidCondition(session, condition.getConditionProviderId())) {
|
||||
throw new ClientPolicyException("Policy " + proposedPolicyRep.getName() + " contains invalid condition " + condition.getConditionProviderId());
|
||||
}
|
||||
policyRep.getConditions().add(condition);
|
||||
}
|
||||
}
|
||||
|
||||
policyRep.setProfiles(new ArrayList<>());
|
||||
if (proposedPolicyRep.getProfiles() != null) {
|
||||
if (proposedPolicyRep.getProfiles().size() != proposedPolicyRep.getProfiles().stream().distinct().count()) {
|
||||
throw new ClientPolicyException("Policy " + proposedPolicyRep.getName() + " contains duplicated profiles");
|
||||
}
|
||||
for (String profile : proposedPolicyRep.getProfiles()) {
|
||||
if (!profileNames.contains(profile)) {
|
||||
throw new ClientPolicyException("Policy " + proposedPolicyRep.getName() + " contains invalid profile " + profile);
|
||||
}
|
||||
policyRep.getProfiles().add(profile);
|
||||
}
|
||||
}
|
||||
|
||||
updatingPolicyList.add(policyRep);
|
||||
}
|
||||
|
||||
return updatingPolicyList;
|
||||
}
|
||||
|
||||
/**
|
||||
* get validated and modified client policies as representation.
|
||||
* it can be constructed by merging proposed client policies with existing client policies.
|
||||
|
@ -405,73 +547,17 @@ public class ClientPoliciesUtil {
|
|||
* @param proposedPoliciesRep
|
||||
*/
|
||||
static ClientPoliciesRepresentation getValidatedClientPoliciesForUpdate(KeycloakSession session, RealmModel realm,
|
||||
ClientPoliciesRepresentation proposedPoliciesRep, List<ClientProfileRepresentation> existingGlobalProfiles) throws ClientPolicyException {
|
||||
ClientPoliciesRepresentation proposedPoliciesRep, List<ClientProfileRepresentation> existingGlobalProfiles,
|
||||
List<ClientPolicyRepresentation> existingGlobalPolicies) throws ClientPolicyException {
|
||||
if (realm == null) {
|
||||
throw new ClientPolicyException("realm not specified.");
|
||||
}
|
||||
|
||||
// no policy contained (it is valid)
|
||||
List<ClientPolicyRepresentation> proposedPolicyRepList = proposedPoliciesRep.getPolicies();
|
||||
if (proposedPolicyRepList == null || proposedPolicyRepList.isEmpty()) {
|
||||
proposedPolicyRepList = new ArrayList<>();
|
||||
proposedPoliciesRep.setPolicies(new ArrayList<>());
|
||||
}
|
||||
|
||||
// Policy without name not allowed
|
||||
if (proposedPolicyRepList.stream().anyMatch(clientPolicy -> clientPolicy.getName() == null || clientPolicy.getName().isEmpty())) {
|
||||
throw new ClientPolicyException("proposed client policy name missing.");
|
||||
}
|
||||
|
||||
// duplicated policy name is not allowed.
|
||||
if (proposedPolicyRepList.size() != proposedPolicyRepList.stream().map(i->i.getName()).distinct().count()) {
|
||||
throw new ClientPolicyException("proposed client policy name duplicated.");
|
||||
}
|
||||
|
||||
// construct updating policies from existing policies and proposed policies
|
||||
ClientPoliciesRepresentation updatingPoliciesRep = new ClientPoliciesRepresentation();
|
||||
updatingPoliciesRep.setPolicies(new ArrayList<>());
|
||||
List<ClientPolicyRepresentation> updatingPoliciesList = updatingPoliciesRep.getPolicies();
|
||||
|
||||
for (ClientPolicyRepresentation proposedPolicyRep : proposedPoliciesRep.getPolicies()) {
|
||||
// newly proposed builtin policy not allowed because builtin policy cannot added/deleted/modified.
|
||||
Boolean enabled = (proposedPolicyRep.isEnabled() != null) ? proposedPolicyRep.isEnabled() : Boolean.FALSE;
|
||||
|
||||
// basically, proposed policy totally overrides existing policy except for enabled field..
|
||||
ClientPolicyRepresentation policyRep = new ClientPolicyRepresentation();
|
||||
policyRep.setName(proposedPolicyRep.getName());
|
||||
policyRep.setDescription(proposedPolicyRep.getDescription());
|
||||
policyRep.setEnabled(enabled);
|
||||
|
||||
policyRep.setConditions(new ArrayList<>());
|
||||
if (proposedPolicyRep.getConditions() != null) {
|
||||
for (ClientPolicyConditionRepresentation conditionRep : proposedPolicyRep.getConditions()) {
|
||||
if (!isValidCondition(session, conditionRep.getConditionProviderId())) {
|
||||
throw new ClientPolicyException("the proposed client policy contains the condition with its invalid configuration.");
|
||||
}
|
||||
policyRep.getConditions().add(conditionRep);
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> existingProfileNames = existingGlobalProfiles.stream().map(ClientProfileRepresentation::getName).collect(Collectors.toSet());
|
||||
ClientProfilesRepresentation reps = getClientProfilesRepresentation(session, realm);
|
||||
policyRep.setProfiles(new ArrayList<>());
|
||||
if (reps.getProfiles() != null) {
|
||||
existingProfileNames.addAll(reps.getProfiles().stream()
|
||||
.map(ClientProfileRepresentation::getName)
|
||||
.collect(Collectors.toSet()));
|
||||
}
|
||||
if (proposedPolicyRep.getProfiles() != null) {
|
||||
for (String profileName : proposedPolicyRep.getProfiles()) {
|
||||
if (!existingProfileNames.contains(profileName)) {
|
||||
logger.warnf("Client policy %s referred not existing profile %s");
|
||||
throw new ClientPolicyException("referring not existing client profile not allowed.");
|
||||
}
|
||||
}
|
||||
proposedPolicyRep.getProfiles().stream().distinct().forEach(profileName->policyRep.getProfiles().add(profileName));
|
||||
}
|
||||
|
||||
updatingPoliciesList.add(policyRep);
|
||||
}
|
||||
List<ClientProfileRepresentation> allProfiles = new ArrayList<>(getClientProfilesRepresentation(session, realm).getProfiles());
|
||||
allProfiles.addAll(existingGlobalProfiles);
|
||||
updatingPoliciesRep.setPolicies(validatePolicies(session, proposedPoliciesRep.getPolicies(), allProfiles, existingGlobalPolicies));
|
||||
|
||||
return updatingPoliciesRep;
|
||||
}
|
||||
|
|
|
@ -18,16 +18,13 @@
|
|||
package org.keycloak.services.clientpolicy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
|
||||
import org.keycloak.representations.idm.ClientProfileRepresentation;
|
||||
import org.keycloak.representations.idm.ClientProfilesRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProvider;
|
||||
|
@ -42,11 +39,9 @@ public class DefaultClientPolicyManager implements ClientPolicyManager {
|
|||
private static final Logger logger = Logger.getLogger(DefaultClientPolicyManager.class);
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final Supplier<List<ClientProfileRepresentation>> globalClientProfilesSupplier;
|
||||
|
||||
public DefaultClientPolicyManager(KeycloakSession session, Supplier<List<ClientProfileRepresentation>> globalClientProfilesSupplier) {
|
||||
public DefaultClientPolicyManager(KeycloakSession session) {
|
||||
this.session = session;
|
||||
this.globalClientProfilesSupplier = globalClientProfilesSupplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -143,7 +138,8 @@ public class DefaultClientPolicyManager implements ClientPolicyManager {
|
|||
ClientProfilesRepresentation clientProfiles = ClientPoliciesUtil.getClientProfilesRepresentation(session, realm);
|
||||
|
||||
for (String profileName : policy.getProfiles()) {
|
||||
ClientProfile profile = ClientPoliciesUtil.getClientProfileModel(session, realm, clientProfiles, globalClientProfilesSupplier.get(), profileName);
|
||||
ClientProfile profile = ClientPoliciesUtil.getClientProfileModel(session, realm, clientProfiles,
|
||||
ClientPoliciesUtil.getGlobalClientProfiles(session), profileName);
|
||||
if (profile == null) {
|
||||
logger.tracev("PROFILE NOT FOUND :: policy name = {0}, profile name = {1}", policy.getName(), profileName);
|
||||
continue;
|
||||
|
@ -212,7 +208,8 @@ public class DefaultClientPolicyManager implements ClientPolicyManager {
|
|||
if (clientProfiles == null) {
|
||||
throw new ClientPolicyException("Passing null clientProfiles not allowed");
|
||||
}
|
||||
ClientProfilesRepresentation validatedProfilesRep = ClientPoliciesUtil.getValidatedClientProfilesForUpdate(session, realm, clientProfiles, globalClientProfilesSupplier.get());
|
||||
ClientProfilesRepresentation validatedProfilesRep = ClientPoliciesUtil.getValidatedClientProfilesForUpdate(session, realm, clientProfiles,
|
||||
ClientPoliciesUtil.getGlobalClientProfiles(session));
|
||||
String validatedJsonString = ClientPoliciesUtil.convertClientProfilesRepresentationToJson(validatedProfilesRep);
|
||||
ClientPoliciesUtil.setClientProfilesJsonString(realm, validatedJsonString);
|
||||
logger.tracev("UPDATE PROFILES :: realm = {0}, validated and modified PUT = {1}", realm.getName(), validatedJsonString);
|
||||
|
@ -227,7 +224,7 @@ public class DefaultClientPolicyManager implements ClientPolicyManager {
|
|||
try {
|
||||
ClientProfilesRepresentation clientProfiles = ClientPoliciesUtil.getClientProfilesRepresentation(session, realm);
|
||||
if (includeGlobalProfiles) {
|
||||
clientProfiles.setGlobalProfiles(new LinkedList<>(globalClientProfilesSupplier.get()));
|
||||
clientProfiles.setGlobalProfiles(ClientPoliciesUtil.getGlobalClientProfiles(session));
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
|
@ -250,7 +247,8 @@ public class DefaultClientPolicyManager implements ClientPolicyManager {
|
|||
if (clientPolicies == null) {
|
||||
throw new ClientPolicyException("Passing null clientPolicies not allowed");
|
||||
}
|
||||
ClientPoliciesRepresentation clientPoliciesRep = ClientPoliciesUtil.getValidatedClientPoliciesForUpdate(session, realm, clientPolicies, globalClientProfilesSupplier.get());
|
||||
ClientPoliciesRepresentation clientPoliciesRep = ClientPoliciesUtil.getValidatedClientPoliciesForUpdate(session, realm, clientPolicies,
|
||||
ClientPoliciesUtil.getGlobalClientProfiles(session), ClientPoliciesUtil.getGlobalClientPolicies(session));
|
||||
validatedJsonString = ClientPoliciesUtil.convertClientPoliciesRepresentationToJson(clientPoliciesRep);
|
||||
} catch (ClientPolicyException e) {
|
||||
logger.warnv("VALIDATE SERIALIZE POLICIES FAILED :: error = {0}, error detail = {1}", e.getError(), e.getErrorDetail());
|
||||
|
@ -261,9 +259,12 @@ public class DefaultClientPolicyManager implements ClientPolicyManager {
|
|||
}
|
||||
|
||||
@Override
|
||||
public ClientPoliciesRepresentation getClientPolicies(RealmModel realm) throws ClientPolicyException {
|
||||
public ClientPoliciesRepresentation getClientPolicies(RealmModel realm, boolean includeGlobalPolicies) throws ClientPolicyException {
|
||||
try {
|
||||
ClientPoliciesRepresentation clientPolicies = ClientPoliciesUtil.getClientPoliciesRepresentation(session, realm);
|
||||
if (includeGlobalPolicies) {
|
||||
clientPolicies.setGlobalPolicies(ClientPoliciesUtil.getGlobalClientPolicies(session));
|
||||
}
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracev("GET POLICIES :: realm = {0}, GET = {1}", realm.getName(), JsonSerialization.writeValueAsString(clientPolicies));
|
||||
}
|
||||
|
@ -283,7 +284,7 @@ public class DefaultClientPolicyManager implements ClientPolicyManager {
|
|||
ClientProfilesRepresentation filteredOutProfiles = getClientProfiles(realm, false);
|
||||
rep.setParsedClientProfiles(filteredOutProfiles);
|
||||
|
||||
ClientPoliciesRepresentation filteredOutPolicies = getClientPolicies(realm);
|
||||
ClientPoliciesRepresentation filteredOutPolicies = getClientPolicies(realm, false);
|
||||
rep.setParsedClientPolicies(filteredOutPolicies);
|
||||
} catch (ClientPolicyException cpe) {
|
||||
throw new IllegalStateException("Exception during export client profiles or client policies", cpe);
|
||||
|
|
|
@ -18,13 +18,10 @@
|
|||
|
||||
package org.keycloak.services.clientpolicy;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.representations.idm.ClientProfileRepresentation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -33,14 +30,9 @@ public class DefaultClientPolicyManagerFactory implements ClientPolicyManagerFac
|
|||
|
||||
private static final Logger logger = Logger.getLogger(DefaultClientPolicyManagerFactory.class);
|
||||
|
||||
// Global (builtin) profiles are loaded on booting keycloak at once.
|
||||
// therefore, their representations are kept and remain unchanged.
|
||||
// these are shared among all realms.
|
||||
private volatile List<ClientProfileRepresentation> globalClientProfiles;
|
||||
|
||||
@Override
|
||||
public ClientPolicyManager create(KeycloakSession session) {
|
||||
return new DefaultClientPolicyManager(session, () -> getGlobalClientProfiles(session));
|
||||
return new DefaultClientPolicyManager(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -62,26 +54,4 @@ public class DefaultClientPolicyManagerFactory implements ClientPolicyManagerFac
|
|||
public String getId() {
|
||||
return "default";
|
||||
}
|
||||
|
||||
/**
|
||||
* When this method is called, assumption is that CLIENT_POLICIES feature is enabled
|
||||
*/
|
||||
protected List<ClientProfileRepresentation> getGlobalClientProfiles(KeycloakSession session) {
|
||||
if (globalClientProfiles == null) {
|
||||
synchronized (this) {
|
||||
if (globalClientProfiles == null) {
|
||||
logger.trace("LOAD GLOBAL CLIENT PROFILES ON KEYCLOAK");
|
||||
|
||||
// load builtin profiles from keycloak-services
|
||||
try {
|
||||
this.globalClientProfiles = ClientPoliciesUtil.getValidatedGlobalClientProfilesRepresentation(session, getClass().getResourceAsStream("/keycloak-default-client-profiles.json"));
|
||||
} catch (ClientPolicyException cpe) {
|
||||
logger.warnv("LOAD GLOBAL PROFILES ON KEYCLOAK FAILED :: error = {0}, error detail = {1}", cpe.getError(), cpe.getErrorDetail());
|
||||
throw new IllegalStateException(cpe);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return globalClientProfiles;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,11 +18,11 @@
|
|||
package org.keycloak.services.resources.admin;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
|
@ -66,11 +66,11 @@ public class ClientPoliciesResource {
|
|||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
|
||||
@Operation()
|
||||
public ClientPoliciesRepresentation getPolicies() {
|
||||
public ClientPoliciesRepresentation getPolicies(@QueryParam("include-global-policies") boolean includeGlobalPolicies) {
|
||||
auth.realm().requireViewRealm();
|
||||
|
||||
try {
|
||||
return session.clientPolicy().getClientPolicies(realm);
|
||||
return session.clientPolicy().getClientPolicies(realm, includeGlobalPolicies);
|
||||
} catch (ClientPolicyException e) {
|
||||
throw ErrorResponse.error(e.getError(), Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.services.securityprofile;
|
||||
|
||||
import java.util.List;
|
||||
import org.keycloak.representations.idm.ClientPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.ClientProfileRepresentation;
|
||||
import org.keycloak.representations.idm.SecurityProfileConfiguration;
|
||||
import org.keycloak.securityprofile.SecurityProfileProvider;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class DefaultSecurityProfileProvider implements SecurityProfileProvider {
|
||||
|
||||
private SecurityProfileConfiguration config;
|
||||
|
||||
public DefaultSecurityProfileProvider(SecurityProfileConfiguration config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return config.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ClientProfileRepresentation> getDefaultClientProfiles() {
|
||||
return config.getDefaultClientProfiles();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ClientPolicyRepresentation> getDefaultClientPolicies() {
|
||||
return config.getDefaultClientPolicies();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
config = null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.services.securityprofile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
import org.keycloak.representations.idm.SecurityProfileConfiguration;
|
||||
import org.keycloak.securityprofile.SecurityProfileProvider;
|
||||
import org.keycloak.securityprofile.SecurityProfileProviderFactory;
|
||||
import org.keycloak.services.clientpolicy.ClientPoliciesUtil;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
/**
|
||||
* The default implementation for the security profile. It reads the configuration
|
||||
* from the file configured.
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class DefaultSecurityProfileProviderFactory implements SecurityProfileProviderFactory {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(DefaultSecurityProfileProviderFactory.class);
|
||||
|
||||
private String name;
|
||||
private volatile SecurityProfileConfiguration configuration;
|
||||
|
||||
@Override
|
||||
public SecurityProfileProvider create(KeycloakSession session) {
|
||||
return new DefaultSecurityProfileProvider(readConfiguration(session));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigMetadata() {
|
||||
return ProviderConfigurationBuilder.create()
|
||||
.property()
|
||||
.name("name")
|
||||
.type("string")
|
||||
.helpText("Name for the security configuration file to use. File `name`.json is searched in classapth and `conf` installation folder.")
|
||||
.add()
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
this.name = config.get("name", "none-security-profile");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "default";
|
||||
}
|
||||
|
||||
protected SecurityProfileConfiguration readConfiguration(KeycloakSession session) {
|
||||
if (configuration == null) {
|
||||
synchronized (this) {
|
||||
SecurityProfileConfiguration conf;
|
||||
final String file = name + ".json";
|
||||
try {
|
||||
// first try to read the json configuration file from classpath
|
||||
InputStream tmp = getClass().getResourceAsStream("/" + file);
|
||||
if (tmp == null) {
|
||||
Path path = Paths.get(System.getProperty("jboss.server.config.dir")).resolve(file);
|
||||
if (!Files.isReadable(path)) {
|
||||
throw new IOException(String.format("File %s does not exists in the conf folder", file));
|
||||
}
|
||||
tmp = Files.newInputStream(path);
|
||||
}
|
||||
try (InputStream is = tmp) {
|
||||
conf = JsonSerialization.readValue(is, SecurityProfileConfiguration.class);
|
||||
}
|
||||
// read the list of client profiles and policies validated
|
||||
conf.setDefaultClientProfiles(ClientPoliciesUtil.readGlobalClientProfilesRepresentation(session, conf.getClientProfiles()));
|
||||
conf.setDefaultClientPolicies(ClientPoliciesUtil.readGlobalClientPoliciesRepresentation(session, conf.getClientPolicies(),
|
||||
conf.getDefaultClientProfiles()));
|
||||
} catch (ClientPolicyException|IOException e) {
|
||||
throw new IllegalStateException("Error loading the security profile from file " + file, e);
|
||||
}
|
||||
this.configuration = conf;
|
||||
}
|
||||
}
|
||||
return this.configuration;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.services.securityprofile.DefaultSecurityProfileProviderFactory
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"policies": [
|
||||
{
|
||||
"name": "Openid-connect OAuth 2.1 confidential client",
|
||||
"description": "Openid-connect confidential client policy to ensure OAuth 2.1 specification.",
|
||||
"enabled": true,
|
||||
"conditions": [
|
||||
{
|
||||
"condition": "client-type",
|
||||
"configuration": {
|
||||
"protocol": "openid-connect"
|
||||
}
|
||||
},
|
||||
{
|
||||
"condition": "client-access-type",
|
||||
"configuration": {
|
||||
"type": [
|
||||
"confidential"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"profiles": [
|
||||
"oauth-2-1-for-confidential-client"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Openid-connect OAuth 2.1 public client",
|
||||
"description": "Openid-connect confidential client policy to ensure OAuth 2.1 specification.",
|
||||
"enabled": true,
|
||||
"conditions": [
|
||||
{
|
||||
"condition": "client-type",
|
||||
"configuration": {
|
||||
"protocol": "openid-connect"
|
||||
}
|
||||
},
|
||||
{
|
||||
"condition": "client-access-type",
|
||||
"configuration": {
|
||||
"type": [
|
||||
"public"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"profiles": [
|
||||
"oauth-2-1-for-public-client"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Saml secure client (signatures, post, https)",
|
||||
"description": "Saml policy to ensure signatures, POST binding and https URLs.",
|
||||
"enabled": true,
|
||||
"conditions": [
|
||||
{
|
||||
"condition": "client-type",
|
||||
"configuration": {
|
||||
"protocol": "saml"
|
||||
}
|
||||
}
|
||||
],
|
||||
"profiles": [
|
||||
"saml-security-profile"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
5
services/src/main/resources/lax-security-profile.json
Normal file
5
services/src/main/resources/lax-security-profile.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name":"lax",
|
||||
"client-profiles":"keycloak-default-client-profiles",
|
||||
"client-policies":null
|
||||
}
|
5
services/src/main/resources/none-security-profile.json
Normal file
5
services/src/main/resources/none-security-profile.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name":"none",
|
||||
"client-profiles":"keycloak-default-client-profiles",
|
||||
"client-policies":null
|
||||
}
|
5
services/src/main/resources/strict-security-profile.json
Normal file
5
services/src/main/resources/strict-security-profile.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name":"strict",
|
||||
"client-profiles":"keycloak-default-client-profiles",
|
||||
"client-policies":"keycloak-strict-client-policies"
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.services.securityprofile;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.crypto.CryptoIntegration;
|
||||
import org.keycloak.common.crypto.CryptoProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.securityprofile.SecurityProfileProvider;
|
||||
import org.keycloak.securityprofile.SecurityProfileProviderFactory;
|
||||
import org.keycloak.services.resteasy.ResteasyKeycloakSession;
|
||||
import org.keycloak.services.resteasy.ResteasyKeycloakSessionFactory;
|
||||
import org.keycloak.utils.ScopeUtil;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
@RunWith(Parameterized.class)
|
||||
public class DefaultSecurityProfileProverFactoryTest {
|
||||
|
||||
private static KeycloakSession session;
|
||||
private final String name;
|
||||
|
||||
@Parameters
|
||||
public static Collection<Object[]> data() {
|
||||
// return of json profile files packed with keycloak
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"none-security-profile"},
|
||||
{"lax-security-profile"},
|
||||
{"strict-security-profile"},
|
||||
});
|
||||
}
|
||||
|
||||
public DefaultSecurityProfileProverFactoryTest(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeClass() {
|
||||
Profile.defaults();
|
||||
CryptoIntegration.init(CryptoProvider.class.getClassLoader());
|
||||
ResteasyKeycloakSessionFactory sessionFactory = new ResteasyKeycloakSessionFactory();
|
||||
sessionFactory.init();
|
||||
session = new ResteasyKeycloakSession(sessionFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigurationFile() {
|
||||
SecurityProfileProviderFactory fact = new DefaultSecurityProfileProviderFactory();
|
||||
fact.init(ScopeUtil.createScope(Collections.singletonMap("name", name)));
|
||||
SecurityProfileProvider prov = fact.create(session);
|
||||
Assert.assertNotNull(prov.getName());
|
||||
Assert.assertNotNull(prov.getDefaultClientProfiles());
|
||||
Assert.assertNotNull(prov.getDefaultClientPolicies());
|
||||
}
|
||||
}
|
|
@ -35,4 +35,10 @@ public @interface SetDefaultProvider {
|
|||
boolean beforeEnableFeature() default true;
|
||||
|
||||
String defaultProvider() default "";
|
||||
|
||||
/**
|
||||
* Configuration for the provider in the form option1, value1, option2, value2
|
||||
* @return The config options and values
|
||||
*/
|
||||
String[] config() default {};
|
||||
}
|
||||
|
|
|
@ -9,9 +9,12 @@ import org.keycloak.testsuite.arquillian.containers.AbstractQuarkusDeployableCon
|
|||
import org.keycloak.utils.StringUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SpiProvidersSwitchingUtils {
|
||||
|
||||
|
@ -28,24 +31,58 @@ public class SpiProvidersSwitchingUtils {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void setDefaultProvider(Container container, String spiName, String providerId) {
|
||||
public void setDefaultProvider(Container container, String spiName, String providerId, String... config) {
|
||||
System.setProperty(getProviderPropertyName(spiName), providerId);
|
||||
if (config != null) {
|
||||
String optionName = null;
|
||||
for (String c : config) {
|
||||
if (optionName == null) {
|
||||
optionName = c;
|
||||
} else {
|
||||
System.setProperty(getProviderPropertyNameConfig(spiName, providerId, optionName), c);
|
||||
optionName = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeProviderConfig(Container container, String spiName) {
|
||||
System.clearProperty(getProviderPropertyName(spiName));
|
||||
List<String> toRemove = System.getProperties().stringPropertyNames().stream()
|
||||
.filter(p -> p.startsWith(getProviderPropertyNamePrefix(spiName)))
|
||||
.collect(Collectors.toList());
|
||||
toRemove.forEach(p -> System.clearProperty(p));
|
||||
}
|
||||
|
||||
private String getProviderPropertyNamePrefix(String spiName) {
|
||||
return "keycloak." + spiName + ".";
|
||||
}
|
||||
|
||||
private String getProviderPropertyName(String spiName) {
|
||||
return "keycloak." + spiName + ".provider";
|
||||
return getProviderPropertyNamePrefix(spiName) + "provider";
|
||||
}
|
||||
|
||||
private String getProviderPropertyNameConfig(String spiName, String providerId, String configName) {
|
||||
return getProviderPropertyNamePrefix(spiName) + providerId + "." + configName;
|
||||
}
|
||||
},
|
||||
QUARKUS {
|
||||
@Override
|
||||
public void setDefaultProvider(Container container, String spiName, String providerId) {
|
||||
getQuarkusContainer(container).setAdditionalBuildArgs(Collections
|
||||
.singletonList(KEYCLOAKX_ARG_SPI_PREFIX + toDashCase(spiName) + "-provider=" + providerId));
|
||||
public void setDefaultProvider(Container container, String spiName, String providerId, String... config) {
|
||||
List<String> args = new LinkedList<>();
|
||||
args.add(KEYCLOAKX_ARG_SPI_PREFIX + toDashCase(spiName) + "-provider=" + providerId);
|
||||
if (config != null) {
|
||||
String optionName = null;
|
||||
for (String c : config) {
|
||||
if (optionName == null) {
|
||||
optionName = c;
|
||||
} else {
|
||||
args.add(KEYCLOAKX_ARG_SPI_PREFIX + toDashCase(spiName) + "-" + providerId + "-" + optionName + "=" + c);
|
||||
optionName = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
getQuarkusContainer(container).setAdditionalBuildArgs(args);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -92,10 +129,10 @@ public class SpiProvidersSwitchingUtils {
|
|||
return Optional.empty();
|
||||
}
|
||||
|
||||
public abstract void setDefaultProvider(Container container, String spiName, String providerId);
|
||||
public abstract void setDefaultProvider(Container container, String spiName, String providerId, String... config);
|
||||
|
||||
public void updateDefaultProvider(Container container, String spiName, String providerId) {
|
||||
setDefaultProvider(container, spiName, providerId);
|
||||
public void updateDefaultProvider(Container container, String spiName, String providerId, String... config) {
|
||||
setDefaultProvider(container, spiName, providerId, config);
|
||||
}
|
||||
|
||||
public void unsetDefaultProvider(Container container, String spiName) {
|
||||
|
@ -125,9 +162,9 @@ public class SpiProvidersSwitchingUtils {
|
|||
|
||||
if (annotation.onlyUpdateDefault()) {
|
||||
spiSwitcher.getCurrentDefaultProvider(container, spi, annotation).ifPresent(v -> originalSettingsBackup.put(spi, v));
|
||||
spiSwitcher.updateDefaultProvider(container, spi, annotation.providerId());
|
||||
spiSwitcher.updateDefaultProvider(container, spi, annotation.providerId(), annotation.config());
|
||||
} else {
|
||||
spiSwitcher.setDefaultProvider(container, spi, annotation.providerId());
|
||||
spiSwitcher.setDefaultProvider(container, spi, annotation.providerId(), annotation.config());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -304,7 +304,6 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
|
|||
createClientScopesConditionConfig(ClientScopesConditionFactory.OPTIONAL, Arrays.asList(SAMPLE_CLIENT_ROLE)))
|
||||
.addProfile("ordinal-test-profile")
|
||||
.addProfile("lack-of-builtin-field-test-profile")
|
||||
.addProfile("ordinal-test-profile")
|
||||
|
||||
.toRepresentation();
|
||||
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.testsuite.securityprofile;
|
||||
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Collectors;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
||||
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
|
||||
import org.keycloak.representations.idm.ClientPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
|
||||
import org.keycloak.testsuite.auth.page.login.OIDCLogin;
|
||||
import org.keycloak.testsuite.util.ClientBuilder;
|
||||
|
||||
@SetDefaultProvider(spi = "security-profile", providerId = "default", config = {"name", "strict-security-profile"})
|
||||
public class StrictSecurityProfileTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGlobalClientPolicies() {
|
||||
RealmResource realm = testRealm();
|
||||
// test there are policies defined in the endpoint
|
||||
ClientPoliciesRepresentation policies = realm.clientPoliciesPoliciesResource().getPolicies(true);
|
||||
Assert.assertNotNull(policies.getGlobalPolicies());
|
||||
MatcherAssert.assertThat(policies.getGlobalPolicies().stream().map(ClientPolicyRepresentation::getName).collect(Collectors.toList()),
|
||||
Matchers.containsInAnyOrder("Openid-connect OAuth 2.1 confidential client",
|
||||
"Openid-connect OAuth 2.1 public client",
|
||||
"Saml secure client (signatures, post, https)"));
|
||||
// try creating a global policy fails
|
||||
ClientPoliciesRepresentation policiesRep = new ClientPoliciesRepresentation();
|
||||
policiesRep.setPolicies(Collections.singletonList(policies.getGlobalPolicies().iterator().next()));
|
||||
BadRequestException e = Assert.assertThrows(BadRequestException.class,
|
||||
() -> realm.clientPoliciesPoliciesResource().updatePolicies(policiesRep));
|
||||
ErrorRepresentation error = e.getResponse().readEntity(ErrorRepresentation.class);
|
||||
MatcherAssert.assertThat(error.getErrorMessage(), Matchers.containsString("duplicated as a global policy"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreatePublicOpenIdConnectClientSecure() {
|
||||
RealmResource realm = testRealm();
|
||||
ClientRepresentation clientRep = ClientBuilder.create()
|
||||
.name("test-client-policy-app")
|
||||
.clientId("test-client-policy-app")
|
||||
.publicClient()
|
||||
.protocol(OIDCLogin.OIDC)
|
||||
.baseUrl("https://www.keycloak.org")
|
||||
.redirectUris("https://www.keycloak.org")
|
||||
.build();
|
||||
clientRep.setImplicitFlowEnabled(true);
|
||||
OIDCAdvancedConfigWrapper wrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep);
|
||||
wrapper.setPostLogoutRedirectUris(Collections.singletonList("https://www.keycloak.org"));
|
||||
wrapper.setPkceCodeChallengeMethod(OIDCLoginProtocol.PKCE_METHOD_PLAIN);
|
||||
|
||||
// set the redirect uri to unsecure http
|
||||
clientRep.setRedirectUris(Collections.singletonList("http://www.keycloak.org"));
|
||||
Response resp = realm.clients().create(clientRep);
|
||||
Assert.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), resp.getStatus());
|
||||
OAuth2ErrorRepresentation error = resp.readEntity(OAuth2ErrorRepresentation.class);
|
||||
Assert.assertEquals(OAuthErrorException.INVALID_REQUEST, error.getError());
|
||||
Assert.assertEquals("Invalid Redirect Uri: invalid uri", error.getErrorDescription());
|
||||
clientRep.setRedirectUris(Collections.singletonList("https://www.keycloak.org"));
|
||||
|
||||
// create OK
|
||||
resp = realm.clients().create(clientRep);
|
||||
Assert.assertEquals(Response.Status.CREATED.getStatusCode(), resp.getStatus());
|
||||
String id = ApiUtil.getCreatedId(resp);
|
||||
getCleanup().addClientUuid(id);
|
||||
|
||||
// check everything is auto-configure for security as the policy has auto-configure
|
||||
clientRep = realm.clients().get(id).toRepresentation();
|
||||
wrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep);
|
||||
Assert.assertEquals(OIDCLoginProtocol.PKCE_METHOD_S256, wrapper.getPkceCodeChallengeMethod());
|
||||
Assert.assertFalse(clientRep.isImplicitFlowEnabled());
|
||||
Assert.assertFalse(clientRep.isDirectAccessGrantsEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateConfidentialOpenIdConnectClientSecure() {
|
||||
RealmResource realm = testRealm();
|
||||
ClientRepresentation clientRep = ClientBuilder.create()
|
||||
.name("test-client-policy-app")
|
||||
.clientId("test-client-policy-app")
|
||||
.protocol(OIDCLogin.OIDC)
|
||||
.baseUrl("https://www.keycloak.org")
|
||||
.redirectUris("https://www.keycloak.org")
|
||||
.directAccessGrants()
|
||||
.build();
|
||||
clientRep.setImplicitFlowEnabled(true);
|
||||
OIDCAdvancedConfigWrapper wrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep);
|
||||
wrapper.setPostLogoutRedirectUris(Collections.singletonList("https://www.keycloak.org"));
|
||||
wrapper.setPkceCodeChallengeMethod(OIDCLoginProtocol.PKCE_METHOD_PLAIN);
|
||||
wrapper.setUseMtlsHoKToken(false);
|
||||
|
||||
// set the redirect uri to unsecure http
|
||||
clientRep.setRedirectUris(Collections.singletonList("http://www.keycloak.org"));
|
||||
Response resp = realm.clients().create(clientRep);
|
||||
Assert.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), resp.getStatus());
|
||||
OAuth2ErrorRepresentation error = resp.readEntity(OAuth2ErrorRepresentation.class);
|
||||
Assert.assertEquals(OAuthErrorException.INVALID_REQUEST, error.getError());
|
||||
Assert.assertEquals("Invalid Redirect Uri: invalid uri", error.getErrorDescription());
|
||||
clientRep.setRedirectUris(Collections.singletonList("https://www.keycloak.org"));
|
||||
|
||||
// create OK
|
||||
resp = realm.clients().create(clientRep);
|
||||
Assert.assertEquals(Response.Status.CREATED.getStatusCode(), resp.getStatus());
|
||||
String id = ApiUtil.getCreatedId(resp);
|
||||
getCleanup().addClientUuid(id);
|
||||
|
||||
// check everything is auto-configure for security as the policy has auto-configure
|
||||
clientRep = realm.clients().get(id).toRepresentation();
|
||||
wrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep);
|
||||
Assert.assertEquals(OIDCLoginProtocol.PKCE_METHOD_S256, wrapper.getPkceCodeChallengeMethod());
|
||||
Assert.assertTrue(wrapper.isUseMtlsHokToken());
|
||||
Assert.assertFalse(clientRep.isImplicitFlowEnabled());
|
||||
Assert.assertFalse(clientRep.isDirectAccessGrantsEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateSamlClientSecure() {
|
||||
RealmResource realm = testRealm();
|
||||
// setup a perfect client for SAML secure
|
||||
ClientRepresentation clientRep = ClientBuilder.create()
|
||||
.name("test-client-policy-app")
|
||||
.clientId("test-client-policy-app")
|
||||
.protocol(OIDCLogin.SAML)
|
||||
.baseUrl("https://www.keycloak.org")
|
||||
.redirectUris("https://www.keycloak.org")
|
||||
.adminUrl("https://www.keycloak.org")
|
||||
.attribute(SamlConfigAttributes.SAML_FORCE_POST_BINDING, "true")
|
||||
.attribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "true")
|
||||
.attribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "true")
|
||||
.build();
|
||||
|
||||
// change base url to unsecure http
|
||||
clientRep.setBaseUrl("http://www.keycloak.org");
|
||||
Response resp = realm.clients().create(clientRep);
|
||||
Assert.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), resp.getStatus());
|
||||
OAuth2ErrorRepresentation error = resp.readEntity(OAuth2ErrorRepresentation.class);
|
||||
Assert.assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, error.getError());
|
||||
MatcherAssert.assertThat(error.getErrorDescription(), Matchers.startsWith("Non secure scheme for"));
|
||||
clientRep.setBaseUrl("https://www.keycloak.org");
|
||||
|
||||
// change force post to false
|
||||
clientRep.getAttributes().put(SamlConfigAttributes.SAML_FORCE_POST_BINDING, "false");
|
||||
resp = realm.clients().create(clientRep);
|
||||
Assert.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), resp.getStatus());
|
||||
error = resp.readEntity(OAuth2ErrorRepresentation.class);
|
||||
Assert.assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, error.getError());
|
||||
Assert.assertEquals("Force POST binding is not enabled", error.getErrorDescription());
|
||||
clientRep.getAttributes().put(SamlConfigAttributes.SAML_FORCE_POST_BINDING, "true");
|
||||
|
||||
// remove client signature
|
||||
clientRep.getAttributes().put(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false");
|
||||
resp = realm.clients().create(clientRep);
|
||||
Assert.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), resp.getStatus());
|
||||
error = resp.readEntity(OAuth2ErrorRepresentation.class);
|
||||
Assert.assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, error.getError());
|
||||
Assert.assertEquals("Signatures not ensured for the client. Ensure Client signature required and Sign documents or Sign assertions are ON",
|
||||
error.getErrorDescription());
|
||||
clientRep.getAttributes().put(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "true");
|
||||
|
||||
// create OK
|
||||
resp = realm.clients().create(clientRep);
|
||||
Assert.assertEquals(Response.Status.CREATED.getStatusCode(), resp.getStatus());
|
||||
getCleanup().addClientUuid(ApiUtil.getCreatedId(resp));
|
||||
}
|
||||
}
|
|
@ -265,5 +265,12 @@
|
|||
|
||||
"saml-artifact-resolver": {
|
||||
"provider": "${keycloak.saml-artifact-resolver.provider:default}"
|
||||
},
|
||||
|
||||
"security-profile": {
|
||||
"provider": "${keycloak.security-profile.provider:default}",
|
||||
"default": {
|
||||
"name":"${keycloak.security-profile.default.name:none-security-profile}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ providers,4
|
|||
runonserver,6
|
||||
saml,6
|
||||
script,6
|
||||
securityprofile,4
|
||||
session,6
|
||||
sessionlimits,6
|
||||
ssl,6
|
||||
|
|
|
@ -184,6 +184,12 @@
|
|||
"sslCertChainPrefix": "x-ssl-client-cert-chain",
|
||||
"certificateChainLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"security-profile": {
|
||||
"provider": "${keycloak.security-profile.provider:default}",
|
||||
"default": {
|
||||
"name":"${keycloak.security-profile.default.name:none-security-profile}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue