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 {
|
public class ClientPoliciesRepresentation {
|
||||||
protected List<ClientPolicyRepresentation> policies = new ArrayList<>();
|
protected List<ClientPolicyRepresentation> policies = new ArrayList<>();
|
||||||
|
private List<ClientPolicyRepresentation> globalPolicies;
|
||||||
|
|
||||||
public List<ClientPolicyRepresentation> getPolicies() {
|
public List<ClientPolicyRepresentation> getPolicies() {
|
||||||
return policies;
|
return policies;
|
||||||
|
@ -39,6 +40,14 @@ public class ClientPoliciesRepresentation {
|
||||||
this.policies = policies;
|
this.policies = policies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<ClientPolicyRepresentation> getGlobalPolicies() {
|
||||||
|
return globalPolicies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGlobalPolicies(List<ClientPolicyRepresentation> globalPolicies) {
|
||||||
|
this.globalPolicies = globalPolicies;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return JsonSerialization.mapper.convertValue(this, JsonNode.class).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.GET;
|
||||||
import javax.ws.rs.PUT;
|
import javax.ws.rs.PUT;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
|
||||||
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
|
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
|
||||||
|
@ -17,6 +18,10 @@ public interface ClientPoliciesPoliciesResource {
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
ClientPoliciesRepresentation getPolicies();
|
ClientPoliciesRepresentation getPolicies();
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
ClientPoliciesRepresentation getPolicies(@QueryParam("include-global-policies") Boolean includeGlobalPolicies);
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
void updatePolicies(final ClientPoliciesRepresentation clientPolicies);
|
void updatePolicies(final ClientPoliciesRepresentation clientPolicies);
|
||||||
|
|
|
@ -1586,6 +1586,7 @@ times.minutes=Minutes
|
||||||
disableUserInfo=Disable user info
|
disableUserInfo=Disable user info
|
||||||
authorizationEncryptedResponseEnc=Authorization response encryption content encryption algorithm
|
authorizationEncryptedResponseEnc=Authorization response encryption content encryption algorithm
|
||||||
editCondition=Edit condition
|
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.
|
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
|
forcePostBinding=Force POST binding
|
||||||
usersExplain=Users are the users in the current realm.
|
usersExplain=Users are the users in the current realm.
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
Flex,
|
Flex,
|
||||||
FlexItem,
|
FlexItem,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
|
Label,
|
||||||
PageSection,
|
PageSection,
|
||||||
Text,
|
Text,
|
||||||
TextVariants,
|
TextVariants,
|
||||||
|
@ -67,7 +68,12 @@ export default function NewClientPolicy() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { realm } = useRealm();
|
const { realm } = useRealm();
|
||||||
const { addAlert, addError } = useAlerts();
|
const { addAlert, addError } = useAlerts();
|
||||||
|
const [isGlobalPolicy, setIsGlobalPolicy] = useState(false);
|
||||||
const [policies, setPolicies] = useState<ClientPolicyRepresentation[]>();
|
const [policies, setPolicies] = useState<ClientPolicyRepresentation[]>();
|
||||||
|
const [globalPolicies, setGlobalPolicies] =
|
||||||
|
useState<ClientPolicyRepresentation[]>();
|
||||||
|
const [allPolicies, setAllPolicies] =
|
||||||
|
useState<ClientPolicyRepresentation[]>();
|
||||||
const [clientProfiles, setClientProfiles] = useState<
|
const [clientProfiles, setClientProfiles] = useState<
|
||||||
ClientProfileRepresentation[]
|
ClientProfileRepresentation[]
|
||||||
>([]);
|
>([]);
|
||||||
|
@ -101,7 +107,9 @@ export default function NewClientPolicy() {
|
||||||
useFetch(
|
useFetch(
|
||||||
async () => {
|
async () => {
|
||||||
const [policies, profiles] = await Promise.all([
|
const [policies, profiles] = await Promise.all([
|
||||||
adminClient.clientPolicies.listPolicies(),
|
adminClient.clientPolicies.listPolicies({
|
||||||
|
includeGlobalPolicies: true,
|
||||||
|
}),
|
||||||
adminClient.clientPolicies.listProfiles({
|
adminClient.clientPolicies.listProfiles({
|
||||||
includeGlobalProfiles: true,
|
includeGlobalProfiles: true,
|
||||||
}),
|
}),
|
||||||
|
@ -110,16 +118,29 @@ export default function NewClientPolicy() {
|
||||||
return { policies, profiles };
|
return { policies, profiles };
|
||||||
},
|
},
|
||||||
({ policies, profiles }) => {
|
({ policies, profiles }) => {
|
||||||
const currentPolicy = policies.policies?.find(
|
let currentPolicy = policies.policies?.find(
|
||||||
(item) => item.name === policyName,
|
(item) => item.name === policyName,
|
||||||
);
|
);
|
||||||
|
if (currentPolicy === undefined) {
|
||||||
|
currentPolicy = policies.globalPolicies?.find(
|
||||||
|
(item) => item.name === policyName,
|
||||||
|
);
|
||||||
|
setIsGlobalPolicy(currentPolicy !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
const allClientProfiles = [
|
const allClientProfiles = [
|
||||||
...(profiles.globalProfiles ?? []),
|
...(profiles.globalProfiles ?? []),
|
||||||
...(profiles.profiles ?? []),
|
...(profiles.profiles ?? []),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const allClientPolicies = [
|
||||||
|
...(policies.globalPolicies ?? []),
|
||||||
|
...(policies.policies ?? []),
|
||||||
|
];
|
||||||
|
|
||||||
setPolicies(policies.policies ?? []);
|
setPolicies(policies.policies ?? []);
|
||||||
|
setGlobalPolicies(policies.globalPolicies ?? []);
|
||||||
|
setAllPolicies(allClientPolicies);
|
||||||
if (currentPolicy) {
|
if (currentPolicy) {
|
||||||
setupForm(currentPolicy);
|
setupForm(currentPolicy);
|
||||||
setClientProfiles(allClientProfiles);
|
setClientProfiles(allClientProfiles);
|
||||||
|
@ -134,7 +155,7 @@ export default function NewClientPolicy() {
|
||||||
form.reset(policy);
|
form.reset(policy);
|
||||||
};
|
};
|
||||||
|
|
||||||
const policy = (policies || []).filter(
|
const policy = (allPolicies || []).filter(
|
||||||
(policy) => policy.name === policyName,
|
(policy) => policy.name === policyName,
|
||||||
);
|
);
|
||||||
const policyConditions = policy[0]?.conditions || [];
|
const policyConditions = policy[0]?.conditions || [];
|
||||||
|
@ -151,8 +172,6 @@ export default function NewClientPolicy() {
|
||||||
const createdForm = form.getValues();
|
const createdForm = form.getValues();
|
||||||
const createdPolicy = {
|
const createdPolicy = {
|
||||||
...createdForm,
|
...createdForm,
|
||||||
profiles: [],
|
|
||||||
conditions: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAllPolicies = () => {
|
const getAllPolicies = () => {
|
||||||
|
@ -279,6 +298,7 @@ export default function NewClientPolicy() {
|
||||||
policies: policies,
|
policies: policies,
|
||||||
});
|
});
|
||||||
addAlert(t("deleteClientPolicyProfileSuccess"), AlertVariant.success);
|
addAlert(t("deleteClientPolicyProfileSuccess"), AlertVariant.success);
|
||||||
|
form.setValue("profiles", currentPolicy?.profiles || []);
|
||||||
navigate(toEditClientPolicy({ realm, policyName: formValues.name! }));
|
navigate(toEditClientPolicy({ realm, policyName: formValues.name! }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addError(t("deleteClientPolicyProfileError"), error);
|
addError(t("deleteClientPolicyProfileError"), error);
|
||||||
|
@ -346,6 +366,10 @@ export default function NewClientPolicy() {
|
||||||
policies: newPolicies,
|
policies: newPolicies,
|
||||||
});
|
});
|
||||||
setPolicies(newPolicies);
|
setPolicies(newPolicies);
|
||||||
|
const allClientPolicies = [...(globalPolicies || []), ...newPolicies];
|
||||||
|
setAllPolicies(allClientPolicies);
|
||||||
|
setCurrentPolicy(createdPolicy);
|
||||||
|
form.setValue("profiles", createdPolicy.profiles);
|
||||||
navigate(toEditClientPolicy({ realm, policyName: formValues.name! }));
|
navigate(toEditClientPolicy({ realm, policyName: formValues.name! }));
|
||||||
addAlert(t("addClientProfileSuccess"), AlertVariant.success);
|
addAlert(t("addClientProfileSuccess"), AlertVariant.success);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -393,9 +417,20 @@ export default function NewClientPolicy() {
|
||||||
? policyName
|
? policyName
|
||||||
: "createPolicy"
|
: "createPolicy"
|
||||||
}
|
}
|
||||||
|
badges={[
|
||||||
|
{
|
||||||
|
id: "global-client-policy-badge",
|
||||||
|
text: isGlobalPolicy ? (
|
||||||
|
<Label color="blue">{t("global")}</Label>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
divider
|
divider
|
||||||
dropdownItems={
|
dropdownItems={
|
||||||
showAddConditionsAndProfilesForm || policyName
|
(showAddConditionsAndProfilesForm || policyName) &&
|
||||||
|
!isGlobalPolicy
|
||||||
? [
|
? [
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="delete"
|
key="delete"
|
||||||
|
@ -410,6 +445,7 @@ export default function NewClientPolicy() {
|
||||||
]
|
]
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
isReadOnly={isGlobalPolicy}
|
||||||
isEnabled={field.value}
|
isEnabled={field.value}
|
||||||
onToggle={(value) => {
|
onToggle={(value) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
|
@ -455,7 +491,7 @@ export default function NewClientPolicy() {
|
||||||
variant="primary"
|
variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
data-testid="saveCreatePolicy"
|
data-testid="saveCreatePolicy"
|
||||||
isDisabled={!form.formState.isValid}
|
isDisabled={!form.formState.isValid || isGlobalPolicy}
|
||||||
>
|
>
|
||||||
{t("save")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -463,7 +499,8 @@ export default function NewClientPolicy() {
|
||||||
id="cancelCreatePolicy"
|
id="cancelCreatePolicy"
|
||||||
variant="link"
|
variant="link"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
showAddConditionsAndProfilesForm || policyName
|
(showAddConditionsAndProfilesForm || policyName) &&
|
||||||
|
!isGlobalPolicy
|
||||||
? reset()
|
? reset()
|
||||||
: navigate(
|
: navigate(
|
||||||
toClientPolicies({
|
toClientPolicies({
|
||||||
|
@ -474,7 +511,9 @@ export default function NewClientPolicy() {
|
||||||
}
|
}
|
||||||
data-testid="cancelCreatePolicy"
|
data-testid="cancelCreatePolicy"
|
||||||
>
|
>
|
||||||
{showAddConditionsAndProfilesForm ? t("reload") : t("cancel")}
|
{showAddConditionsAndProfilesForm && !isGlobalPolicy
|
||||||
|
? t("reload")
|
||||||
|
: t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</ActionGroup>
|
</ActionGroup>
|
||||||
{(showAddConditionsAndProfilesForm ||
|
{(showAddConditionsAndProfilesForm ||
|
||||||
|
@ -490,26 +529,28 @@ export default function NewClientPolicy() {
|
||||||
/>
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
</FlexItem>
|
</FlexItem>
|
||||||
<FlexItem align={{ default: "alignRight" }}>
|
{!isGlobalPolicy && (
|
||||||
<Button
|
<FlexItem align={{ default: "alignRight" }}>
|
||||||
id="addCondition"
|
<Button
|
||||||
component={(props) => (
|
id="addCondition"
|
||||||
<Link
|
component={(props) => (
|
||||||
{...props}
|
<Link
|
||||||
to={toNewClientPolicyCondition({
|
{...props}
|
||||||
realm,
|
to={toNewClientPolicyCondition({
|
||||||
policyName: policyName!,
|
realm,
|
||||||
})}
|
policyName: policyName!,
|
||||||
></Link>
|
})}
|
||||||
)}
|
></Link>
|
||||||
variant="link"
|
)}
|
||||||
className="kc-addCondition"
|
variant="link"
|
||||||
data-testid="addCondition"
|
className="kc-addCondition"
|
||||||
icon={<PlusCircleIcon />}
|
data-testid="addCondition"
|
||||||
>
|
icon={<PlusCircleIcon />}
|
||||||
{t("addCondition")}
|
>
|
||||||
</Button>
|
{t("addCondition")}
|
||||||
</FlexItem>
|
</Button>
|
||||||
|
</FlexItem>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
{policyConditions.length > 0 ? (
|
{policyConditions.length > 0 ? (
|
||||||
<DataList aria-label={t("conditions")} isCompact>
|
<DataList aria-label={t("conditions")} isCompact>
|
||||||
|
@ -552,24 +593,26 @@ export default function NewClientPolicy() {
|
||||||
helpText={type.helpText}
|
helpText={type.helpText}
|
||||||
fieldLabelId={condition.condition}
|
fieldLabelId={condition.condition}
|
||||||
/>
|
/>
|
||||||
<Button
|
{!isGlobalPolicy && (
|
||||||
variant="link"
|
<Button
|
||||||
aria-label="remove-condition"
|
variant="link"
|
||||||
isInline
|
aria-label="remove-condition"
|
||||||
icon={
|
isInline
|
||||||
<TrashIcon
|
icon={
|
||||||
className="kc-conditionType-trash-icon"
|
<TrashIcon
|
||||||
data-testid={`delete-${condition.condition}-condition`}
|
className="kc-conditionType-trash-icon"
|
||||||
onClick={() => {
|
data-testid={`delete-${condition.condition}-condition`}
|
||||||
toggleDeleteConditionDialog();
|
onClick={() => {
|
||||||
setConditionToDelete({
|
toggleDeleteConditionDialog();
|
||||||
idx: idx,
|
setConditionToDelete({
|
||||||
name: type.id!,
|
idx: idx,
|
||||||
});
|
name: type.id!,
|
||||||
}}
|
});
|
||||||
/>
|
}}
|
||||||
}
|
/>
|
||||||
></Button>
|
}
|
||||||
|
></Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
|
@ -609,18 +652,20 @@ export default function NewClientPolicy() {
|
||||||
/>
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
</FlexItem>
|
</FlexItem>
|
||||||
<FlexItem align={{ default: "alignRight" }}>
|
{!isGlobalPolicy && (
|
||||||
<Button
|
<FlexItem align={{ default: "alignRight" }}>
|
||||||
id="addClientProfile"
|
<Button
|
||||||
variant="link"
|
id="addClientProfile"
|
||||||
className="kc-addClientProfile"
|
variant="link"
|
||||||
data-testid="addClientProfile"
|
className="kc-addClientProfile"
|
||||||
icon={<PlusCircleIcon />}
|
data-testid="addClientProfile"
|
||||||
onClick={toggleModal}
|
icon={<PlusCircleIcon />}
|
||||||
>
|
onClick={toggleModal}
|
||||||
{t("addClientProfile")}
|
>
|
||||||
</Button>
|
{t("addClientProfile")}
|
||||||
</FlexItem>
|
</Button>
|
||||||
|
</FlexItem>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
{policyProfiles.length > 0 ? (
|
{policyProfiles.length > 0 ? (
|
||||||
<DataList aria-label={t("profiles")} isCompact>
|
<DataList aria-label={t("profiles")} isCompact>
|
||||||
|
@ -663,24 +708,26 @@ export default function NewClientPolicy() {
|
||||||
}
|
}
|
||||||
fieldLabelId={profile}
|
fieldLabelId={profile}
|
||||||
/>
|
/>
|
||||||
<Button
|
{!isGlobalPolicy && (
|
||||||
variant="link"
|
<Button
|
||||||
aria-label="remove-client-profile"
|
variant="link"
|
||||||
isInline
|
aria-label="remove-client-profile"
|
||||||
icon={
|
isInline
|
||||||
<TrashIcon
|
icon={
|
||||||
className="kc-conditionType-trash-icon"
|
<TrashIcon
|
||||||
data-testid="deleteClientProfileDropdown"
|
className="kc-conditionType-trash-icon"
|
||||||
onClick={() => {
|
data-testid="deleteClientProfileDropdown"
|
||||||
toggleDeleteProfileDialog();
|
onClick={() => {
|
||||||
setProfileToDelete({
|
toggleDeleteProfileDialog();
|
||||||
idx: idx,
|
setProfileToDelete({
|
||||||
name: type!,
|
idx: idx,
|
||||||
});
|
name: type!,
|
||||||
}}
|
});
|
||||||
/>
|
}}
|
||||||
}
|
/>
|
||||||
></Button>
|
}
|
||||||
|
></Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
|
|
|
@ -18,12 +18,13 @@ import { camelCase } from "lodash-es";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||||
import { FormPanel, HelpItem } from "ui-shared";
|
import { HelpItem } from "ui-shared";
|
||||||
import { adminClient } from "../admin-client";
|
import { adminClient } from "../admin-client";
|
||||||
import { useAlerts } from "../components/alert/Alerts";
|
import { useAlerts } from "../components/alert/Alerts";
|
||||||
import { DynamicComponents } from "../components/dynamic/DynamicComponents";
|
import { DynamicComponents } from "../components/dynamic/DynamicComponents";
|
||||||
import { FormAccess } from "../components/form/FormAccess";
|
import { FormAccess } from "../components/form/FormAccess";
|
||||||
|
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||||
import { useRealm } from "../context/realm-context/RealmContext";
|
import { useRealm } from "../context/realm-context/RealmContext";
|
||||||
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
||||||
import { useFetch } from "../utils/useFetch";
|
import { useFetch } from "../utils/useFetch";
|
||||||
|
@ -44,6 +45,7 @@ export default function NewClientPolicyCondition() {
|
||||||
const { realm } = useRealm();
|
const { realm } = useRealm();
|
||||||
|
|
||||||
const [openConditionType, setOpenConditionType] = useState(false);
|
const [openConditionType, setOpenConditionType] = useState(false);
|
||||||
|
const [isGlobalPolicy, setIsGlobalPolicy] = useState(false);
|
||||||
const [policies, setPolicies] = useState<ClientPolicyRepresentation[]>([]);
|
const [policies, setPolicies] = useState<ClientPolicyRepresentation[]>([]);
|
||||||
|
|
||||||
const [condition, setCondition] = useState<
|
const [condition, setCondition] = useState<
|
||||||
|
@ -72,15 +74,24 @@ export default function NewClientPolicyCondition() {
|
||||||
};
|
};
|
||||||
|
|
||||||
useFetch(
|
useFetch(
|
||||||
() => adminClient.clientPolicies.listPolicies(),
|
() =>
|
||||||
|
adminClient.clientPolicies.listPolicies({
|
||||||
|
includeGlobalPolicies: true,
|
||||||
|
}),
|
||||||
|
|
||||||
(policies) => {
|
(policies) => {
|
||||||
setPolicies(policies.policies ?? []);
|
setPolicies(policies.policies ?? []);
|
||||||
|
|
||||||
if (conditionName) {
|
if (conditionName) {
|
||||||
const currentPolicy = policies.policies?.find(
|
let currentPolicy = policies.policies?.find(
|
||||||
(item) => item.name === policyName,
|
(item) => item.name === policyName,
|
||||||
);
|
);
|
||||||
|
if (currentPolicy === undefined) {
|
||||||
|
currentPolicy = policies.globalPolicies?.find(
|
||||||
|
(item) => item.name === policyName,
|
||||||
|
);
|
||||||
|
setIsGlobalPolicy(currentPolicy !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
const typeAndConfigData = currentPolicy?.conditions?.find(
|
const typeAndConfigData = currentPolicy?.conditions?.find(
|
||||||
(item) => item.condition === conditionName,
|
(item) => item.condition === conditionName,
|
||||||
|
@ -170,14 +181,22 @@ export default function NewClientPolicyCondition() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection variant="light">
|
<>
|
||||||
<FormPanel
|
<ViewHeader
|
||||||
className="kc-login-screen"
|
titleKey={
|
||||||
title={conditionName ? t("editCondition") : t("addCondition")}
|
conditionName
|
||||||
>
|
? isGlobalPolicy
|
||||||
|
? t("viewCondition")
|
||||||
|
: t("editCondition")
|
||||||
|
: t("addCondition")
|
||||||
|
}
|
||||||
|
divider
|
||||||
|
/>
|
||||||
|
<PageSection variant="light">
|
||||||
<FormAccess
|
<FormAccess
|
||||||
isHorizontal
|
isHorizontal
|
||||||
role="manage-realm"
|
role="manage-realm"
|
||||||
|
isReadOnly={isGlobalPolicy}
|
||||||
className="pf-v5-u-mt-lg"
|
className="pf-v5-u-mt-lg"
|
||||||
onSubmit={form.handleSubmit(save)}
|
onSubmit={form.handleSubmit(save)}
|
||||||
>
|
>
|
||||||
|
@ -245,27 +264,48 @@ export default function NewClientPolicyCondition() {
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<DynamicComponents properties={conditionProperties} />
|
<DynamicComponents properties={conditionProperties} />
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
<ActionGroup>
|
{!isGlobalPolicy && (
|
||||||
<Button
|
<ActionGroup>
|
||||||
variant="primary"
|
<Button
|
||||||
type="submit"
|
variant="primary"
|
||||||
data-testid="addCondition-saveBtn"
|
type="submit"
|
||||||
isDisabled={conditionType === "" && !conditionName}
|
data-testid="addCondition-saveBtn"
|
||||||
>
|
isDisabled={
|
||||||
{conditionName ? t("save") : t("add")}
|
conditionType === "" && !conditionName && isGlobalPolicy
|
||||||
</Button>
|
}
|
||||||
<Button
|
>
|
||||||
variant="link"
|
{conditionName ? t("save") : t("add")}
|
||||||
data-testid="addCondition-cancelBtn"
|
</Button>
|
||||||
onClick={() =>
|
<Button
|
||||||
navigate(toEditClientPolicy({ realm, policyName: policyName! }))
|
variant="link"
|
||||||
}
|
data-testid="addCondition-cancelBtn"
|
||||||
>
|
onClick={() =>
|
||||||
{t("cancel")}
|
navigate(
|
||||||
</Button>
|
toEditClientPolicy({ realm, policyName: policyName! }),
|
||||||
</ActionGroup>
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
</ActionGroup>
|
||||||
|
)}
|
||||||
</FormAccess>
|
</FormAccess>
|
||||||
</FormPanel>
|
{isGlobalPolicy && (
|
||||||
</PageSection>
|
<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,
|
Divider,
|
||||||
Flex,
|
Flex,
|
||||||
FlexItem,
|
FlexItem,
|
||||||
|
Label,
|
||||||
PageSection,
|
PageSection,
|
||||||
Radio,
|
Radio,
|
||||||
Switch,
|
Switch,
|
||||||
|
@ -36,29 +37,48 @@ import { toEditClientPolicy } from "./routes/EditClientPolicy";
|
||||||
|
|
||||||
import "./realm-settings-section.css";
|
import "./realm-settings-section.css";
|
||||||
|
|
||||||
|
type ClientPolicy = ClientPolicyRepresentation & {
|
||||||
|
global?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export const PoliciesTab = () => {
|
export const PoliciesTab = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { addAlert, addError } = useAlerts();
|
const { addAlert, addError } = useAlerts();
|
||||||
const { realm } = useRealm();
|
const { realm } = useRealm();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
const [policies, setPolicies] = useState<ClientPolicyRepresentation[]>();
|
const [policies, setPolicies] = useState<ClientPolicy[]>();
|
||||||
const [selectedPolicy, setSelectedPolicy] =
|
const [selectedPolicy, setSelectedPolicy] = useState<ClientPolicy>();
|
||||||
useState<ClientPolicyRepresentation>();
|
|
||||||
const [key, setKey] = useState(0);
|
const [key, setKey] = useState(0);
|
||||||
const [code, setCode] = useState<string>();
|
const [code, setCode] = useState<string>();
|
||||||
const [tablePolicies, setTablePolicies] =
|
const [tablePolicies, setTablePolicies] = useState<ClientPolicy[]>();
|
||||||
useState<ClientPolicyRepresentation[]>();
|
|
||||||
const refresh = () => setKey(key + 1);
|
const refresh = () => setKey(key + 1);
|
||||||
|
|
||||||
const form = useForm<Record<string, boolean>>({ mode: "onChange" });
|
const form = useForm<Record<string, boolean>>({ mode: "onChange" });
|
||||||
|
|
||||||
useFetch(
|
useFetch(
|
||||||
() => adminClient.clientPolicies.listPolicies(),
|
() =>
|
||||||
(policies) => {
|
adminClient.clientPolicies.listPolicies({
|
||||||
setPolicies(policies.policies),
|
includeGlobalPolicies: true,
|
||||||
setTablePolicies(policies.policies || []),
|
}),
|
||||||
setCode(prettyPrintJSON(policies.policies));
|
(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],
|
[key],
|
||||||
);
|
);
|
||||||
|
@ -68,16 +88,19 @@ export const PoliciesTab = () => {
|
||||||
const saveStatus = async () => {
|
const saveStatus = async () => {
|
||||||
const switchValues = form.getValues();
|
const switchValues = form.getValues();
|
||||||
|
|
||||||
const updatedPolicies = policies?.map<ClientPolicyRepresentation>(
|
const updatedPolicies = policies
|
||||||
(policy) => {
|
?.filter((policy) => {
|
||||||
|
return !policy.global;
|
||||||
|
})
|
||||||
|
.map<ClientPolicyRepresentation>((policy) => {
|
||||||
const enabled = switchValues[policy.name!];
|
const enabled = switchValues[policy.name!];
|
||||||
|
const enabledPolicy = {
|
||||||
return {
|
|
||||||
...policy,
|
...policy,
|
||||||
enabled,
|
enabled,
|
||||||
};
|
};
|
||||||
},
|
delete enabledPolicy.global;
|
||||||
);
|
return enabledPolicy;
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await adminClient.clientPolicies.updatePolicy({
|
await adminClient.clientPolicies.updatePolicy({
|
||||||
|
@ -90,15 +113,13 @@ export const PoliciesTab = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const ClientPolicyDetailLink = ({ name }: ClientPolicyRepresentation) => (
|
const ClientPolicyDetailLink = (row: ClientPolicy) => (
|
||||||
<Link to={toEditClientPolicy({ realm, policyName: name! })}>{name}</Link>
|
<Link to={toEditClientPolicy({ realm, policyName: row.name! })}>
|
||||||
|
{row.name} {row.global && <Label color="blue">{t("global")}</Label>}
|
||||||
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
||||||
const SwitchRenderer = ({
|
const SwitchRenderer = ({ clientPolicy }: { clientPolicy: ClientPolicy }) => {
|
||||||
clientPolicy,
|
|
||||||
}: {
|
|
||||||
clientPolicy: ClientPolicyRepresentation;
|
|
||||||
}) => {
|
|
||||||
const [toggleDisableDialog, DisableConfirm] = useConfirmDialog({
|
const [toggleDisableDialog, DisableConfirm] = useConfirmDialog({
|
||||||
titleKey: "disablePolicyConfirmTitle",
|
titleKey: "disablePolicyConfirmTitle",
|
||||||
messageKey: "disablePolicyConfirm",
|
messageKey: "disablePolicyConfirm",
|
||||||
|
@ -122,6 +143,7 @@ export const PoliciesTab = () => {
|
||||||
label={t("enabled")}
|
label={t("enabled")}
|
||||||
labelOff={t("disabled")}
|
labelOff={t("disabled")}
|
||||||
isChecked={field.value}
|
isChecked={field.value}
|
||||||
|
isDisabled={clientPolicy.global}
|
||||||
onChange={(_event, value) => {
|
onChange={(_event, value) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
toggleDisableDialog();
|
toggleDisableDialog();
|
||||||
|
@ -144,7 +166,7 @@ export const PoliciesTab = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const obj: ClientPolicyRepresentation[] = JSON.parse(code);
|
const obj: ClientPolicy[] = JSON.parse(code);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await adminClient.clientPolicies.updatePolicy({
|
await adminClient.clientPolicies.updatePolicy({
|
||||||
|
@ -169,9 +191,15 @@ export const PoliciesTab = () => {
|
||||||
continueButtonLabel: t("delete"),
|
continueButtonLabel: t("delete"),
|
||||||
continueButtonVariant: ButtonVariant.danger,
|
continueButtonVariant: ButtonVariant.danger,
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
const updatedPolicies = policies?.filter(
|
const updatedPolicies = policies
|
||||||
(policy) => policy.name !== selectedPolicy?.name,
|
?.filter((policy) => {
|
||||||
);
|
return !policy.global && policy.name !== selectedPolicy?.name;
|
||||||
|
})
|
||||||
|
.map<ClientPolicyRepresentation>((policy) => {
|
||||||
|
const newPolicy = { ...policy };
|
||||||
|
delete newPolicy.global;
|
||||||
|
return newPolicy;
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await adminClient.clientPolicies.updatePolicy({
|
await adminClient.clientPolicies.updatePolicy({
|
||||||
|
@ -250,6 +278,7 @@ export const PoliciesTab = () => {
|
||||||
</Button>
|
</Button>
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
}
|
}
|
||||||
|
isRowDisabled={(value) => !!value.global}
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
title: t("delete"),
|
title: t("delete"),
|
||||||
|
@ -257,7 +286,7 @@ export const PoliciesTab = () => {
|
||||||
toggleDeleteDialog();
|
toggleDeleteDialog();
|
||||||
setSelectedPolicy(item);
|
setSelectedPolicy(item);
|
||||||
},
|
},
|
||||||
} as Action<ClientPolicyRepresentation>,
|
} as Action<ClientPolicy>,
|
||||||
]}
|
]}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,5 +4,6 @@ import type ClientPolicyRepresentation from "./clientPolicyRepresentation.js";
|
||||||
* https://www.keycloak.org/docs-api/15.0/rest-api/#_clientpoliciesrepresentation
|
* https://www.keycloak.org/docs-api/15.0/rest-api/#_clientpoliciesrepresentation
|
||||||
*/
|
*/
|
||||||
export default interface ClientPoliciesRepresentation {
|
export default interface ClientPoliciesRepresentation {
|
||||||
|
globalPolicies?: ClientPolicyRepresentation[];
|
||||||
policies?: ClientPolicyRepresentation[];
|
policies?: ClientPolicyRepresentation[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,9 +38,16 @@ export class ClientPolicies extends Resource<{ realm?: string }> {
|
||||||
|
|
||||||
/* Client Policies */
|
/* Client Policies */
|
||||||
|
|
||||||
public listPolicies = this.makeRequest<{}, ClientPoliciesRepresentation>({
|
public listPolicies = this.makeRequest<
|
||||||
|
{ includeGlobalPolicies?: boolean },
|
||||||
|
ClientPoliciesRepresentation
|
||||||
|
>({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/policies",
|
path: "/policies",
|
||||||
|
queryParamKeys: ["include-global-policies"],
|
||||||
|
keyTransform: {
|
||||||
|
includeGlobalPolicies: "include-global-policies",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
public updatePolicy = this.makeRequest<ClientPoliciesRepresentation, void>({
|
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.health.LoadBalancerCheckSpi
|
||||||
org.keycloak.cookie.CookieSpi
|
org.keycloak.cookie.CookieSpi
|
||||||
org.keycloak.organization.OrganizationSpi
|
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.
|
* 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 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
|
* @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.
|
* 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;
|
package org.keycloak.services.clientpolicy;
|
||||||
|
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
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.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
@ -27,10 +32,7 @@ import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
import org.keycloak.common.Profile;
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.component.ComponentModel;
|
import org.keycloak.component.ComponentModel;
|
||||||
import org.keycloak.component.JsonConfigComponentModel;
|
import org.keycloak.component.JsonConfigComponentModel;
|
||||||
|
@ -38,14 +40,15 @@ import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
|
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepresentation;
|
||||||
import org.keycloak.representations.idm.ClientPolicyConditionRepresentation;
|
import org.keycloak.representations.idm.ClientPolicyConditionRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
|
||||||
import org.keycloak.representations.idm.ClientPolicyExecutorRepresentation;
|
import org.keycloak.representations.idm.ClientPolicyExecutorRepresentation;
|
||||||
import org.keycloak.representations.idm.ClientPolicyRepresentation;
|
import org.keycloak.representations.idm.ClientPolicyRepresentation;
|
||||||
import org.keycloak.representations.idm.ClientProfileRepresentation;
|
import org.keycloak.representations.idm.ClientProfileRepresentation;
|
||||||
import org.keycloak.representations.idm.ClientProfilesRepresentation;
|
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.services.clientpolicy.condition.ClientPolicyConditionProvider;
|
||||||
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
|
|
||||||
import org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider;
|
import org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
@ -58,6 +61,43 @@ public class ClientPoliciesUtil {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(ClientPoliciesUtil.class);
|
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.
|
* gets existing client profiles in a realm as representation.
|
||||||
* not return null.
|
* not return null.
|
||||||
|
@ -183,6 +223,24 @@ public class ClientPoliciesUtil {
|
||||||
return updatingProfileList;
|
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.
|
* convert client profiles as representation to json.
|
||||||
* can return null.
|
* can return null.
|
||||||
|
@ -240,7 +298,7 @@ public class ClientPoliciesUtil {
|
||||||
Set<String> globalProfileNames = globalClientProfiles.stream().map(ClientProfileRepresentation::getName).collect(Collectors.toSet());
|
Set<String> globalProfileNames = globalClientProfiles.stream().map(ClientProfileRepresentation::getName).collect(Collectors.toSet());
|
||||||
for (ClientProfileRepresentation clientProfile : proposedProfileRepList) {
|
for (ClientProfileRepresentation clientProfile : proposedProfileRepList) {
|
||||||
if (globalProfileNames.contains(clientProfile.getName())) {
|
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);
|
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.
|
* Gets existing enabled client policies in a realm.
|
||||||
* not return null.
|
* not return null.
|
||||||
*/
|
*/
|
||||||
static List<ClientPolicy> getEnabledClientPolicies(KeycloakSession session, RealmModel realm) {
|
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
|
// get existing profiles as json
|
||||||
String policiesJson = getClientPoliciesJsonString(realm);
|
String policiesJson = getClientPoliciesJsonString(realm);
|
||||||
if (policiesJson == null) {
|
if (policiesJson != null) {
|
||||||
return Collections.emptyList();
|
// 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (policiesRep.isEmpty()) {
|
||||||
// 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) {
|
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// constructing existing policies (representation -> model)
|
// constructing existing policies (representation -> model)
|
||||||
List<ClientPolicy> policyList = new ArrayList<>();
|
List<ClientPolicy> policyList = new ArrayList<>();
|
||||||
for (ClientPolicyRepresentation policyRep: policiesRep.getPolicies()) {
|
for (ClientPolicyRepresentation policyRep: policiesRep) {
|
||||||
// ignore policy without name
|
// ignore policy without name
|
||||||
if (policyRep.getName() == null) {
|
if (policyRep.getName() == null) {
|
||||||
logger.warnf("Ignored client policy without name in the realm %s", realm.getName());
|
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.
|
* get validated and modified client policies as representation.
|
||||||
* it can be constructed by merging proposed client policies with existing client policies.
|
* it can be constructed by merging proposed client policies with existing client policies.
|
||||||
|
@ -405,73 +547,17 @@ public class ClientPoliciesUtil {
|
||||||
* @param proposedPoliciesRep
|
* @param proposedPoliciesRep
|
||||||
*/
|
*/
|
||||||
static ClientPoliciesRepresentation getValidatedClientPoliciesForUpdate(KeycloakSession session, RealmModel realm,
|
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) {
|
if (realm == null) {
|
||||||
throw new ClientPolicyException("realm not specified.");
|
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();
|
ClientPoliciesRepresentation updatingPoliciesRep = new ClientPoliciesRepresentation();
|
||||||
updatingPoliciesRep.setPolicies(new ArrayList<>());
|
|
||||||
List<ClientPolicyRepresentation> updatingPoliciesList = updatingPoliciesRep.getPolicies();
|
|
||||||
|
|
||||||
for (ClientPolicyRepresentation proposedPolicyRep : proposedPoliciesRep.getPolicies()) {
|
List<ClientProfileRepresentation> allProfiles = new ArrayList<>(getClientProfilesRepresentation(session, realm).getProfiles());
|
||||||
// newly proposed builtin policy not allowed because builtin policy cannot added/deleted/modified.
|
allProfiles.addAll(existingGlobalProfiles);
|
||||||
Boolean enabled = (proposedPolicyRep.isEnabled() != null) ? proposedPolicyRep.isEnabled() : Boolean.FALSE;
|
updatingPoliciesRep.setPolicies(validatePolicies(session, proposedPoliciesRep.getPolicies(), allProfiles, existingGlobalPolicies));
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatingPoliciesRep;
|
return updatingPoliciesRep;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,16 +18,13 @@
|
||||||
package org.keycloak.services.clientpolicy;
|
package org.keycloak.services.clientpolicy;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.common.Profile;
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
|
import org.keycloak.representations.idm.ClientPoliciesRepresentation;
|
||||||
import org.keycloak.representations.idm.ClientProfileRepresentation;
|
|
||||||
import org.keycloak.representations.idm.ClientProfilesRepresentation;
|
import org.keycloak.representations.idm.ClientProfilesRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProvider;
|
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 static final Logger logger = Logger.getLogger(DefaultClientPolicyManager.class);
|
||||||
|
|
||||||
private final KeycloakSession session;
|
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.session = session;
|
||||||
this.globalClientProfilesSupplier = globalClientProfilesSupplier;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -143,7 +138,8 @@ public class DefaultClientPolicyManager implements ClientPolicyManager {
|
||||||
ClientProfilesRepresentation clientProfiles = ClientPoliciesUtil.getClientProfilesRepresentation(session, realm);
|
ClientProfilesRepresentation clientProfiles = ClientPoliciesUtil.getClientProfilesRepresentation(session, realm);
|
||||||
|
|
||||||
for (String profileName : policy.getProfiles()) {
|
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) {
|
if (profile == null) {
|
||||||
logger.tracev("PROFILE NOT FOUND :: policy name = {0}, profile name = {1}", policy.getName(), profileName);
|
logger.tracev("PROFILE NOT FOUND :: policy name = {0}, profile name = {1}", policy.getName(), profileName);
|
||||||
continue;
|
continue;
|
||||||
|
@ -212,7 +208,8 @@ public class DefaultClientPolicyManager implements ClientPolicyManager {
|
||||||
if (clientProfiles == null) {
|
if (clientProfiles == null) {
|
||||||
throw new ClientPolicyException("Passing null clientProfiles not allowed");
|
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);
|
String validatedJsonString = ClientPoliciesUtil.convertClientProfilesRepresentationToJson(validatedProfilesRep);
|
||||||
ClientPoliciesUtil.setClientProfilesJsonString(realm, validatedJsonString);
|
ClientPoliciesUtil.setClientProfilesJsonString(realm, validatedJsonString);
|
||||||
logger.tracev("UPDATE PROFILES :: realm = {0}, validated and modified PUT = {1}", realm.getName(), 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 {
|
try {
|
||||||
ClientProfilesRepresentation clientProfiles = ClientPoliciesUtil.getClientProfilesRepresentation(session, realm);
|
ClientProfilesRepresentation clientProfiles = ClientPoliciesUtil.getClientProfilesRepresentation(session, realm);
|
||||||
if (includeGlobalProfiles) {
|
if (includeGlobalProfiles) {
|
||||||
clientProfiles.setGlobalProfiles(new LinkedList<>(globalClientProfilesSupplier.get()));
|
clientProfiles.setGlobalProfiles(ClientPoliciesUtil.getGlobalClientProfiles(session));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logger.isTraceEnabled()) {
|
if (logger.isTraceEnabled()) {
|
||||||
|
@ -250,7 +247,8 @@ public class DefaultClientPolicyManager implements ClientPolicyManager {
|
||||||
if (clientPolicies == null) {
|
if (clientPolicies == null) {
|
||||||
throw new ClientPolicyException("Passing null clientPolicies not allowed");
|
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);
|
validatedJsonString = ClientPoliciesUtil.convertClientPoliciesRepresentationToJson(clientPoliciesRep);
|
||||||
} catch (ClientPolicyException e) {
|
} catch (ClientPolicyException e) {
|
||||||
logger.warnv("VALIDATE SERIALIZE POLICIES FAILED :: error = {0}, error detail = {1}", e.getError(), e.getErrorDetail());
|
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
|
@Override
|
||||||
public ClientPoliciesRepresentation getClientPolicies(RealmModel realm) throws ClientPolicyException {
|
public ClientPoliciesRepresentation getClientPolicies(RealmModel realm, boolean includeGlobalPolicies) throws ClientPolicyException {
|
||||||
try {
|
try {
|
||||||
ClientPoliciesRepresentation clientPolicies = ClientPoliciesUtil.getClientPoliciesRepresentation(session, realm);
|
ClientPoliciesRepresentation clientPolicies = ClientPoliciesUtil.getClientPoliciesRepresentation(session, realm);
|
||||||
|
if (includeGlobalPolicies) {
|
||||||
|
clientPolicies.setGlobalPolicies(ClientPoliciesUtil.getGlobalClientPolicies(session));
|
||||||
|
}
|
||||||
if (logger.isTraceEnabled()) {
|
if (logger.isTraceEnabled()) {
|
||||||
logger.tracev("GET POLICIES :: realm = {0}, GET = {1}", realm.getName(), JsonSerialization.writeValueAsString(clientPolicies));
|
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);
|
ClientProfilesRepresentation filteredOutProfiles = getClientProfiles(realm, false);
|
||||||
rep.setParsedClientProfiles(filteredOutProfiles);
|
rep.setParsedClientProfiles(filteredOutProfiles);
|
||||||
|
|
||||||
ClientPoliciesRepresentation filteredOutPolicies = getClientPolicies(realm);
|
ClientPoliciesRepresentation filteredOutPolicies = getClientPolicies(realm, false);
|
||||||
rep.setParsedClientPolicies(filteredOutPolicies);
|
rep.setParsedClientPolicies(filteredOutPolicies);
|
||||||
} catch (ClientPolicyException cpe) {
|
} catch (ClientPolicyException cpe) {
|
||||||
throw new IllegalStateException("Exception during export client profiles or client policies", cpe);
|
throw new IllegalStateException("Exception during export client profiles or client policies", cpe);
|
||||||
|
|
|
@ -18,13 +18,10 @@
|
||||||
|
|
||||||
package org.keycloak.services.clientpolicy;
|
package org.keycloak.services.clientpolicy;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.representations.idm.ClientProfileRepresentation;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @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);
|
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
|
@Override
|
||||||
public ClientPolicyManager create(KeycloakSession session) {
|
public ClientPolicyManager create(KeycloakSession session) {
|
||||||
return new DefaultClientPolicyManager(session, () -> getGlobalClientProfiles(session));
|
return new DefaultClientPolicyManager(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -62,26 +54,4 @@ public class DefaultClientPolicyManagerFactory implements ClientPolicyManagerFac
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return "default";
|
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;
|
package org.keycloak.services.resources.admin;
|
||||||
|
|
||||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
import jakarta.ws.rs.BadRequestException;
|
|
||||||
import jakarta.ws.rs.Consumes;
|
import jakarta.ws.rs.Consumes;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.PUT;
|
import jakarta.ws.rs.PUT;
|
||||||
import jakarta.ws.rs.Produces;
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.QueryParam;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
|
|
||||||
|
@ -66,11 +66,11 @@ public class ClientPoliciesResource {
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
|
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
|
||||||
@Operation()
|
@Operation()
|
||||||
public ClientPoliciesRepresentation getPolicies() {
|
public ClientPoliciesRepresentation getPolicies(@QueryParam("include-global-policies") boolean includeGlobalPolicies) {
|
||||||
auth.realm().requireViewRealm();
|
auth.realm().requireViewRealm();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return session.clientPolicy().getClientPolicies(realm);
|
return session.clientPolicy().getClientPolicies(realm, includeGlobalPolicies);
|
||||||
} catch (ClientPolicyException e) {
|
} catch (ClientPolicyException e) {
|
||||||
throw ErrorResponse.error(e.getError(), Response.Status.BAD_REQUEST);
|
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;
|
boolean beforeEnableFeature() default true;
|
||||||
|
|
||||||
String defaultProvider() default "";
|
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 org.keycloak.utils.StringUtil;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class SpiProvidersSwitchingUtils {
|
public class SpiProvidersSwitchingUtils {
|
||||||
|
|
||||||
|
@ -28,24 +31,58 @@ public class SpiProvidersSwitchingUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
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
|
@Override
|
||||||
public void removeProviderConfig(Container container, String spiName) {
|
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) {
|
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 {
|
QUARKUS {
|
||||||
@Override
|
@Override
|
||||||
public void setDefaultProvider(Container container, String spiName, String providerId) {
|
public void setDefaultProvider(Container container, String spiName, String providerId, String... config) {
|
||||||
getQuarkusContainer(container).setAdditionalBuildArgs(Collections
|
List<String> args = new LinkedList<>();
|
||||||
.singletonList(KEYCLOAKX_ARG_SPI_PREFIX + toDashCase(spiName) + "-provider=" + providerId));
|
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
|
@Override
|
||||||
|
@ -92,10 +129,10 @@ public class SpiProvidersSwitchingUtils {
|
||||||
return Optional.empty();
|
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) {
|
public void updateDefaultProvider(Container container, String spiName, String providerId, String... config) {
|
||||||
setDefaultProvider(container, spiName, providerId);
|
setDefaultProvider(container, spiName, providerId, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void unsetDefaultProvider(Container container, String spiName) {
|
public void unsetDefaultProvider(Container container, String spiName) {
|
||||||
|
@ -125,9 +162,9 @@ public class SpiProvidersSwitchingUtils {
|
||||||
|
|
||||||
if (annotation.onlyUpdateDefault()) {
|
if (annotation.onlyUpdateDefault()) {
|
||||||
spiSwitcher.getCurrentDefaultProvider(container, spi, annotation).ifPresent(v -> originalSettingsBackup.put(spi, v));
|
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 {
|
} 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)))
|
createClientScopesConditionConfig(ClientScopesConditionFactory.OPTIONAL, Arrays.asList(SAMPLE_CLIENT_ROLE)))
|
||||||
.addProfile("ordinal-test-profile")
|
.addProfile("ordinal-test-profile")
|
||||||
.addProfile("lack-of-builtin-field-test-profile")
|
.addProfile("lack-of-builtin-field-test-profile")
|
||||||
.addProfile("ordinal-test-profile")
|
|
||||||
|
|
||||||
.toRepresentation();
|
.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": {
|
"saml-artifact-resolver": {
|
||||||
"provider": "${keycloak.saml-artifact-resolver.provider:default}"
|
"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
|
runonserver,6
|
||||||
saml,6
|
saml,6
|
||||||
script,6
|
script,6
|
||||||
|
securityprofile,4
|
||||||
session,6
|
session,6
|
||||||
sessionlimits,6
|
sessionlimits,6
|
||||||
ssl,6
|
ssl,6
|
||||||
|
@ -46,4 +47,4 @@ util,4
|
||||||
validation,6
|
validation,6
|
||||||
vault,4
|
vault,4
|
||||||
welcomepage,6
|
welcomepage,6
|
||||||
x509,4
|
x509,4
|
||||||
|
|
|
@ -184,6 +184,12 @@
|
||||||
"sslCertChainPrefix": "x-ssl-client-cert-chain",
|
"sslCertChainPrefix": "x-ssl-client-cert-chain",
|
||||||
"certificateChainLength": 1
|
"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