Fine grained clients (#2702)

This commit is contained in:
Stan Silvert 2022-05-30 05:23:24 -04:00 committed by GitHub
parent be000ff08c
commit 5b559bcdbd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 153 additions and 75 deletions

View file

@ -56,6 +56,7 @@ export const AdvancedTab = ({
protocol,
authenticationFlowBindingOverrides,
adminUrl,
access,
},
}: AdvancedProps) => {
const { t } = useTranslation("clients");
@ -197,7 +198,11 @@ export const AdvancedTab = ({
tab
</Trans>
</Text>
<FormAccess role="manage-clients" isHorizontal>
<FormAccess
role="manage-clients"
fineGrainedAccess={access?.configure}
isHorizontal
>
<FormGroup
label={t("notBefore")}
fieldId="kc-not-before"
@ -258,7 +263,11 @@ export const AdvancedTab = ({
)}
{publicClient && (
<>
<FormAccess role="manage-clients" isHorizontal>
<FormAccess
role="manage-clients"
fineGrainedAccess={access?.configure}
isHorizontal
>
<FormGroup
label={t("nodeReRegistrationTimeout")}
fieldId="kc-node-reregistration-timeout"
@ -393,6 +402,7 @@ export const AdvancedTab = ({
setValue(`attributes.${key}`, value)
)
}
hasConfigureAccess={access?.configure}
/>
</>
)}
@ -424,6 +434,7 @@ export const AdvancedTab = ({
reset={() =>
resetFields(["exclude.session.state.from.auth.response"])
}
hasConfigureAccess={access?.configure}
/>
</>
)}
@ -434,6 +445,7 @@ export const AdvancedTab = ({
<AdvancedSettings
protocol={protocol}
control={control}
hasConfigureAccess={access?.configure}
save={() => save()}
reset={() => {
resetFields([
@ -463,6 +475,7 @@ export const AdvancedTab = ({
authenticationFlowBindingOverrides?.direct_grant
);
}}
hasConfigureAccess={access?.configure}
/>
</>
</ScrollForm>

View file

@ -11,9 +11,13 @@ import { KeycloakTextArea } from "../components/keycloak-text-area/KeycloakTextA
type ClientDescriptionProps = {
protocol?: string;
hasConfigureAccess?: boolean;
};
export const ClientDescription = ({ protocol }: ClientDescriptionProps) => {
export const ClientDescription = ({
protocol,
hasConfigureAccess: configure,
}: ClientDescriptionProps) => {
const { t } = useTranslation("clients");
const {
register,
@ -21,7 +25,7 @@ export const ClientDescription = ({ protocol }: ClientDescriptionProps) => {
formState: { errors },
} = useFormContext<ClientRepresentation>();
return (
<FormAccess role="manage-clients" unWrap>
<FormAccess role="manage-clients" fineGrainedAccess={configure} unWrap>
<FormGroup
labelIcon={
<HelpItem helpText="clients-help:clientId" fieldLabelId="clientId" />

View file

@ -128,7 +128,7 @@ const ClientDetailHeader = ({
}, [client, t]);
const { hasAccess } = useAccess();
const isManager = hasAccess("manage-clients");
const isManager = hasAccess("manage-clients") || client.access?.configure;
const dropdownItems = [
<DropdownItem key="download" onClick={toggleDownloadDialog}>
@ -189,12 +189,11 @@ export default function ClientDetails() {
const { profileInfo } = useServerInfo();
const { hasAccess } = useAccess();
const isManager = hasAccess("manage-clients");
const canViewPermissions = hasAccess(
"manage-authorization",
"manage-clients"
);
const canViewServiceAccountRoles = hasAccess("view-users");
const permissionsEnabled =
!profileInfo?.disabledFeatures?.includes("ADMIN_FINE_GRAINED_AUTHZ") &&
hasAccess("manage-authorization");
const hasManageClients = hasAccess("manage-clients");
const hasViewUsers = hasAccess("view-users");
const history = useHistory();
@ -432,27 +431,33 @@ export default function ClientDetails() {
{...route("keys")}
>
{client.protocol === "openid-connect" && (
<Keys clientId={clientId} save={save} />
<Keys
clientId={clientId}
save={save}
hasConfigureAccess={client.access?.configure}
/>
)}
{client.protocol === "saml" && (
<SamlKeys clientId={clientId} save={save} />
)}
</Tab>
)}
{!client.publicClient && !isRealmClient(client) && (
<Tab
id="credentials"
title={<TabTitleText>{t("credentials")}</TabTitleText>}
{...route("credentials")}
>
<Credentials
key={key}
client={client}
save={save}
refresh={() => setKey(key + 1)}
/>
</Tab>
)}
{!client.publicClient &&
!isRealmClient(client) &&
(hasManageClients || client.access?.configure) && (
<Tab
id="credentials"
title={<TabTitleText>{t("credentials")}</TabTitleText>}
{...route("credentials")}
>
<Credentials
key={key}
client={client}
save={save}
refresh={() => setKey(key + 1)}
/>
</Tab>
)}
<Tab
id="roles"
data-testid="rolesTab"
@ -463,7 +468,7 @@ export default function ClientDetails() {
loader={loader}
paginated={false}
messageBundle="clients"
isReadOnly={!isManager}
isReadOnly={!(hasManageClients || client.access?.configure)}
/>
</Tab>
{!isRealmClient(client) && !client.bearerOnly && (
@ -496,6 +501,7 @@ export default function ClientDetails() {
clientName={client.clientId!}
clientId={clientId}
protocol={client!.protocol!}
fineGrainedAccess={client!.access?.manage}
/>
</Tab>
<Tab
@ -595,7 +601,7 @@ export default function ClientDetails() {
</RoutableTabs>
</Tab>
)}
{client!.serviceAccountsEnabled && canViewServiceAccountRoles && (
{client!.serviceAccountsEnabled && hasViewUsers && (
<Tab
id="serviceAccount"
data-testid="serviceAccountTab"
@ -605,19 +611,16 @@ export default function ClientDetails() {
<ServiceAccount client={client} />
</Tab>
)}
{!profileInfo?.disabledFeatures?.includes(
"ADMIN_FINE_GRAINED_AUTHZ"
) &&
canViewPermissions && (
<Tab
id="permissions"
data-testid="permissionsTab"
title={<TabTitleText>{t("common:permissions")}</TabTitleText>}
{...route("permissions")}
>
<PermissionsTab id={client.id!} type="clients" />
</Tab>
)}
{permissionsEnabled && (hasManageClients || client.access?.manage) && (
<Tab
id="permissions"
data-testid="permissionsTab"
title={<TabTitleText>{t("common:permissions")}</TabTitleText>}
{...route("permissions")}
>
<PermissionsTab id={client.id!} type="clients" />
</Tab>
)}
<Tab
id="advanced"
data-testid="advancedTab"

View file

@ -49,7 +49,7 @@ export const ClientSettings = ({
const { realm } = useRealm();
const { hasAccess } = useAccess();
const isManager = hasAccess("manage-clients");
const isManager = hasAccess("manage-clients") || client.access?.configure;
const [loginThemeOpen, setLoginThemeOpen] = useState(false);
const loginThemes = useServerInfo().themes!["login"];
@ -88,7 +88,10 @@ export const ClientSettings = ({
sections={sections.map((section) => t(section))}
>
<Form isHorizontal>
<ClientDescription protocol={client.protocol} />
<ClientDescription
protocol={client.protocol}
hasConfigureAccess={client.access?.configure}
/>
</Form>
{protocol === "saml" ? (
<SamlConfig />
@ -96,7 +99,11 @@ export const ClientSettings = ({
!client.bearerOnly && <CapabilityConfig />
)}
{protocol === "saml" && <SamlSignature />}
<FormAccess isHorizontal role="manage-clients">
<FormAccess
isHorizontal
role="manage-clients"
fineGrainedAccess={client.access?.configure}
>
{!client.bearerOnly && (
<>
<FormGroup
@ -259,7 +266,11 @@ export const ClientSettings = ({
/>
)}
</FormAccess>
<FormAccess isHorizontal role="manage-clients">
<FormAccess
isHorizontal
role="manage-clients"
fineGrainedAccess={client.access?.configure}
>
<FormGroup
label={t("loginTheme")}
labelIcon={
@ -383,7 +394,11 @@ export const ClientSettings = ({
/>
)}
</FormAccess>
<FormAccess isHorizontal role="manage-clients">
<FormAccess
isHorizontal
role="manage-clients"
fineGrainedAccess={client.access?.configure}
>
{protocol === "openid-connect" && (
<>
<FormGroup

View file

@ -161,7 +161,10 @@ export default function ClientsSection() {
},
];
if (!isRealmClient(client) && isManager) {
if (
!isRealmClient(client) &&
(isManager || client.access?.configure)
) {
actions.push({
title: t("common:delete"),
onClick() {

View file

@ -68,7 +68,7 @@ export const GeneralSettings = () => {
)}
/>
</FormGroup>
<ClientDescription />
<ClientDescription hasConfigureAccess />
</FormAccess>
);
};

View file

@ -23,6 +23,7 @@ type AdvancedSettingsProps = {
save: () => void;
reset: () => void;
protocol?: string;
hasConfigureAccess?: boolean;
};
export const AdvancedSettings = ({
@ -30,11 +31,16 @@ export const AdvancedSettings = ({
save,
reset,
protocol,
hasConfigureAccess,
}: AdvancedSettingsProps) => {
const { t } = useTranslation("clients");
const [open, setOpen] = useState(false);
return (
<FormAccess role="manage-realm" isHorizontal>
<FormAccess
role="manage-realm"
fineGrainedAccess={hasConfigureAccess}
isHorizontal
>
{protocol !== "openid-connect" && (
<FormGroup
label={t("assertionLifespan")}

View file

@ -20,6 +20,7 @@ type AuthenticationOverridesProps = {
save: () => void;
reset: () => void;
protocol?: string;
hasConfigureAccess?: boolean;
};
export const AuthenticationOverrides = ({
@ -27,6 +28,7 @@ export const AuthenticationOverrides = ({
control,
save,
reset,
hasConfigureAccess,
}: AuthenticationOverridesProps) => {
const adminClient = useAdminClient();
const { t } = useTranslation("clients");
@ -56,7 +58,11 @@ export const AuthenticationOverrides = ({
);
return (
<FormAccess role="manage-clients" isHorizontal>
<FormAccess
role="manage-clients"
fineGrainedAccess={hasConfigureAccess}
isHorizontal
>
<FormGroup
label={t("browserFlow")}
fieldId="browserFlow"

View file

@ -20,11 +20,13 @@ import { KeycloakTextInput } from "../../components/keycloak-text-input/Keycloak
type FineGrainOpenIdConnectProps = {
save: () => void;
reset: () => void;
hasConfigureAccess?: boolean;
};
export const FineGrainOpenIdConnect = ({
save,
reset,
hasConfigureAccess,
}: FineGrainOpenIdConnectProps) => {
const { t } = useTranslation("clients");
const providers = useServerInfo().providers;
@ -141,7 +143,11 @@ export const FineGrainOpenIdConnect = ({
));
return (
<FormAccess role="manage-clients" isHorizontal>
<FormAccess
role="manage-clients"
fineGrainedAccess={hasConfigureAccess}
isHorizontal
>
<FormGroup
label={t("logoUrl")}
fieldId="logoUrl"

View file

@ -10,16 +10,22 @@ type OpenIdConnectCompatibilityModesProps = {
control: Control<Record<string, any>>;
save: () => void;
reset: () => void;
hasConfigureAccess?: boolean;
};
export const OpenIdConnectCompatibilityModes = ({
control,
save,
reset,
hasConfigureAccess,
}: OpenIdConnectCompatibilityModesProps) => {
const { t } = useTranslation("clients");
return (
<FormAccess role="manage-realm" isHorizontal>
<FormAccess
role="manage-realm"
fineGrainedAccess={hasConfigureAccess}
isHorizontal
>
<FormGroup
label={t("excludeSessionStateFromAuthenticationResponse")}
fieldId="excludeSessionStateFromAuthenticationResponse"

View file

@ -347,7 +347,7 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
>
<FormAccess
isHorizontal
role="manage-clients"
role="view-clients"
onSubmit={form.handleSubmit(evaluate)}
>
<FormGroup
@ -487,7 +487,7 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
</FormAccess>
</FormPanel>
<FormPanel className="kc-permissions" title={t("common:permissions")}>
<FormAccess isHorizontal role="manage-clients">
<FormAccess isHorizontal role="view-clients">
<FormGroup
label={t("applyToResourceType")}
fieldId="applyToResourceType"

View file

@ -65,7 +65,7 @@ export const AuthorizationExport = () => {
return (
<PageSection>
<FormAccess isHorizontal role="manage-realm" className="pf-u-mt-lg">
<FormAccess isHorizontal role="view-realm" className="pf-u-mt-lg">
<FormGroup
label={t("authDetails")}
labelIcon={

View file

@ -194,7 +194,7 @@ export default function PermissionDetails() {
<PageSection variant="light">
<FormAccess
isHorizontal
role="manage-clients"
role="view-clients"
onSubmit={handleSubmit(save)}
>
<FormProvider {...form}>

View file

@ -179,7 +179,7 @@ export default function ResourceDetails() {
<FormProvider {...form}>
<FormAccess
isHorizontal
role="manage-clients"
role="view-clients"
className="keycloak__resource-details__form"
onSubmit={handleSubmit(save)}
>

View file

@ -128,7 +128,7 @@ export default function ScopeDetails() {
<PageSection variant="light">
<FormAccess
isHorizontal
role="manage-clients"
role="view-clients"
onSubmit={handleSubmit(save)}
>
<FormGroup

View file

@ -192,7 +192,7 @@ export default function PolicyDetails() {
<FormAccess
isHorizontal
onSubmit={handleSubmit(save)}
role="manage-clients"
role="view-clients"
>
<FormProvider {...form}>
<NameDescription prefix="policy" />

View file

@ -75,7 +75,7 @@ export default function ImportForm() {
>
<FormProvider {...form}>
<JsonFileUpload id="realm-file" onChange={handleFileChange} />
<ClientDescription />
<ClientDescription hasConfigureAccess />
<FormGroup label={t("common:type")} fieldId="kc-type">
<KeycloakTextInput
type="text"

View file

@ -33,11 +33,12 @@ import { Certificate } from "./Certificate";
type KeysProps = {
save: () => void;
clientId: string;
hasConfigureAccess?: boolean;
};
const attr = "jwt.credential";
export const Keys = ({ clientId, save }: KeysProps) => {
export const Keys = ({ clientId, save, hasConfigureAccess }: KeysProps) => {
const { t } = useTranslation("clients");
const {
control,
@ -125,7 +126,11 @@ export const Keys = ({ clientId, save }: KeysProps) => {
</TextContent>
</CardBody>
<CardBody>
<FormAccess role="manage-clients" isHorizontal>
<FormAccess
role="manage-clients"
fineGrainedAccess={hasConfigureAccess}
isHorizontal
>
<FormGroup
hasNoPaddingTop
label={t("useJwksUrl")}

View file

@ -15,7 +15,7 @@ export const DedicatedScopeDetailsRoute: RouteDef = {
path: "/:realm/clients/:clientId/clientScopes/dedicated/:tab?",
component: lazy(() => import("../scopes/DedicatedScopes")),
breadcrumb: (t) => t("clients:dedicatedScopes"),
access: "manage-clients",
access: "view-clients",
};
export const toDedicatedScope = (

View file

@ -16,7 +16,7 @@ export const NewPermissionRoute: RouteDef = {
path: "/:realm/clients/:id/authorization/permission/new/:permissionType/:selectedId?",
component: lazy(() => import("../authorization/PermissionDetails")),
breadcrumb: (t) => t("clients:createPermission"),
access: "manage-clients",
access: "view-clients",
};
export const toNewPermission = (

View file

@ -9,7 +9,7 @@ export const NewPolicyRoute: RouteDef = {
path: "/:realm/clients/:id/authorization/policy/new/:policyType",
component: lazy(() => import("../authorization/policy/PolicyDetails")),
breadcrumb: (t) => t("clients:createPolicy"),
access: "manage-clients",
access: "view-clients",
};
export const toCreatePolicy = (

View file

@ -9,7 +9,7 @@ export const NewResourceRoute: RouteDef = {
path: "/:realm/clients/:id/authorization/resource/new",
component: lazy(() => import("../authorization/ResourceDetails")),
breadcrumb: (t) => t("clients:createResource"),
access: "manage-clients",
access: "view-clients",
};
export const toCreateResource = (

View file

@ -9,7 +9,7 @@ export const NewScopeRoute: RouteDef = {
path: "/:realm/clients/:id/authorization/scope/new",
component: lazy(() => import("../authorization/ScopeDetails")),
breadcrumb: (t) => t("clients:createAuthorizationScope"),
access: "manage-clients",
access: "view-clients",
};
export const toNewScope = (

View file

@ -15,7 +15,7 @@ export const PermissionDetailsRoute: RouteDef = {
path: "/:realm/clients/:id/authorization/permission/:permissionType/:permissionId",
component: lazy(() => import("../authorization/PermissionDetails")),
breadcrumb: (t) => t("clients:permissionDetails"),
access: "manage-clients",
access: "view-clients",
};
export const toPermissionDetails = (

View file

@ -14,7 +14,7 @@ export const PolicyDetailsRoute: RouteDef = {
path: "/:realm/clients/:id/authorization/policy/:policyId/:policyType",
component: lazy(() => import("../authorization/policy/PolicyDetails")),
breadcrumb: (t) => t("clients:createPolicy"),
access: "manage-clients",
access: "view-clients",
};
export const toPolicyDetails = (

View file

@ -13,7 +13,7 @@ export const ResourceDetailsRoute: RouteDef = {
path: "/:realm/clients/:id/authorization/resource/:resourceId?",
component: lazy(() => import("../authorization/ResourceDetails")),
breadcrumb: (t) => t("clients:createResource"),
access: "manage-clients",
access: "view-clients",
};
export const toResourceDetails = (

View file

@ -46,6 +46,7 @@ export type ClientScopesProps = {
clientId: string;
protocol: string;
clientName: string;
fineGrainedAccess?: boolean;
};
export type Row = ClientScopeRepresentation & {
@ -59,6 +60,7 @@ export const ClientScopes = ({
clientId,
protocol,
clientName,
fineGrainedAccess,
}: ClientScopesProps) => {
const { t } = useTranslation("clients");
const adminClient = useAdminClient();
@ -83,7 +85,7 @@ export const ClientScopes = ({
const isDedicatedRow = (value: Row) => value.id === DEDICATED_ROW;
const { hasAccess } = useAccess();
const isManager = hasAccess("manage-clients");
const isManager = hasAccess("manage-clients") || fineGrainedAccess;
const loader = async (first?: number, max?: number, search?: string) => {
const defaultClientScopes =
@ -130,7 +132,7 @@ export const ClientScopes = ({
firstNum,
firstNum + Number(max)
);
if (firstNum === 0) {
if (firstNum === 0 && isManager) {
return [
{
id: DEDICATED_ROW,

View file

@ -20,6 +20,7 @@ import {
} from "../../components/role-mapping/RoleMapping";
import type { RoleMappingPayload } from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
import useToggle from "../../utils/useToggle";
import { useAccess } from "../../context/access/Access";
type DedicatedScopeProps = {
client: ClientRepresentation;
@ -35,6 +36,9 @@ export const DedicatedScope = ({
const [client, setClient] = useState<ClientRepresentation>(initialClient);
const [hide, toggle] = useToggle();
const { hasAccess } = useAccess();
const isManager = hasAccess("manage-clients") || client.access?.manage;
const loader = async () => {
const [clients, assignedRoles, effectiveRoles] = await Promise.all([
adminClient.clients.find(),
@ -118,7 +122,11 @@ export const DedicatedScope = ({
return (
<PageSection>
<FormAccess role="manage-clients" isHorizontal>
<FormAccess
role="manage-clients"
fineGrainedAccess={client.access?.manage}
isHorizontal
>
<FormGroup
hasNoPaddingTop
label={t("fullScopeAllowed")}
@ -149,6 +157,7 @@ export const DedicatedScope = ({
loader={loader}
save={assignRoles}
onHideRolesToggle={toggle}
isManager={isManager}
/>
</>
)}

View file

@ -35,7 +35,7 @@ export const ServiceAccount = ({ client }: ServiceAccountProps) => {
const [serviceAccount, setServiceAccount] = useState<UserRepresentation>();
const { hasAccess } = useAccess();
const isManager = hasAccess("manage-clients");
const hasManageClients = hasAccess("manage-clients");
useFetch(
() =>
@ -128,7 +128,7 @@ export const ServiceAccount = ({ client }: ServiceAccountProps) => {
name={client.clientId!}
id={serviceAccount.id!}
type="users"
isManager={isManager}
isManager={hasManageClients || client.access?.configure}
loader={loader}
save={assignRoles}
onHideRolesToggle={() => setHide(!hide)}