Skip to content

Commit

Permalink
added link to form to create admin user
Browse files Browse the repository at this point in the history
fixes: keycloak#33965
Signed-off-by: Erik Jan de Wit <[email protected]>
  • Loading branch information
edewit committed Nov 18, 2024
1 parent e4509a7 commit 54aa143
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3252,7 +3252,7 @@ userInvitedOrganization_one=Invite to user sent
userInvitedOrganizationError=Could not invite user to the organizations\: {{error}}
userInvitedOrganization_other={{count}} invites to users sent
sentInvitation=Sent invitation
loggedInAsTempAdminUser=You are logged in as a temporary admin user. To harden security, create a permanent admin account and delete the temporary one.
loggedInAsTempAdminUser=You are logged in as a temporary admin user. To harden security, <1>create a permanent admin account</1> and delete the temporary one.
temporaryAdmin=Temporary admin user account. Ensure it is replaced with a permanent admin user account as soon as possible.
temporaryService=Temporary admin service account. Ensure it is replaced with a permanent admin service account as soon as possible.
addOrganizationAttributes.label=Add organization attributes
Expand Down
32 changes: 27 additions & 5 deletions js/apps/admin-ui/src/Banners.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
import { Banner, Flex, FlexItem } from "@patternfly/react-core";
import { ExclamationTriangleIcon } from "@patternfly/react-icons";
import { ReactNode } from "react";
import { Trans, useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { useRealm } from "./context/realm-context/RealmContext";
import { useWhoAmI } from "./context/whoami/WhoAmI";
import { useTranslation } from "react-i18next";
import { toAddAdminUser } from "./user/routes/AddUser";

type WarnBannerProps = {
msg: string;
msg: string | ReactNode;
};

const WarnBanner = ({ msg }: WarnBannerProps) => {
const { t } = useTranslation();

return (
<Banner screenReaderText={t(msg)} variant="gold" isSticky>
<Banner
screenReaderText={typeof msg === "string" ? t(msg) : ""}
variant="gold"
isSticky
>
<Flex
spaceItems={{ default: "spaceItemsSm" }}
flexWrap={{ default: "wrap" }}
>
<FlexItem style={{ whiteSpace: "normal" }}>
<ExclamationTriangleIcon style={{ marginRight: "0.3rem" }} />
{t(msg)}
{typeof msg === "string" ? t(msg) : msg}
</FlexItem>
</Flex>
</Banner>
Expand All @@ -27,6 +35,20 @@ const WarnBanner = ({ msg }: WarnBannerProps) => {

export const Banners = () => {
const { whoAmI } = useWhoAmI();
const { realm } = useRealm();

if (whoAmI.isTemporary()) return <WarnBanner msg="loggedInAsTempAdminUser" />;
if (whoAmI.isTemporary())
return (
<WarnBanner
msg={
<Trans i18nKey="loggedInAsTempAdminUser">
You are logged in as a temporary admin user. To harden security,
<Link to={toAddAdminUser({ realm })}>
create a permanent admin account
</Link>
and delete the temporary one.
</Trans>
}
/>
);
};
128 changes: 128 additions & 0 deletions js/apps/admin-ui/src/user/CreateAdminUser.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { UserProfileMetadata } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
import {
isUserProfileError,
KeycloakSpinner,
setUserProfileServerError,
useAlerts,
useFetch,
UserProfileFields,
} from "@keycloak/keycloak-ui-shared";
import { PageSection } from "@patternfly/react-core";
import { TFunction } from "i18next";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useAdminClient } from "../admin-client";
import { FixedButtonsGroup } from "../components/form/FixedButtonGroup";
import { FormAccess } from "../components/form/FormAccess";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { useRealm } from "../context/realm-context/RealmContext";
import { useWhoAmI } from "../context/whoami/WhoAmI";
import { toUserRepresentation, UserFormFields } from "./form-state";
import { toUser } from "./routes/User";
import { toUsers } from "./routes/Users";
import { ResetPasswordForm } from "./user-credentials/ResetPasswordForm";

type AdminUserFields = UserFormFields & {
password: string;
passwordConfirmation: string;
};

export default function CreateAdminUser() {
const { t } = useTranslation();
const form = useForm<AdminUserFields>({ mode: "onChange" });
const { handleSubmit } = form;

const { whoAmI } = useWhoAmI();
const currentLocale = whoAmI.getLocale();
const { adminClient } = useAdminClient();
const { addAlert, addError } = useAlerts();
const navigate = useNavigate();
const { realm: realmName, realmRepresentation: realm } = useRealm();
const [userProfileMetadata, setUserProfileMetadata] =
useState<UserProfileMetadata>();

useFetch(
() => adminClient.users.getProfileMetadata({ realm: realmName }),
(userProfileMetadata) => {
if (!userProfileMetadata) {
throw new Error(t("notFound"));
}

form.setValue("attributes.locale", realm?.defaultLocale || "");
setUserProfileMetadata(userProfileMetadata);
},
[],
);

const save = async (data: AdminUserFields) => {
try {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password, passwordConfirmation, ...user } = data;
const createdUser = await adminClient.users.create({
...toUserRepresentation(user),
enabled: true,
});
await adminClient.users.resetPassword({
id: createdUser.id!,
credential: {
type: "password",
value: password,
},
});
const role = await adminClient.roles.findOneByName({ name: "admin" });
await adminClient.users.addRealmRoleMappings({
id: createdUser.id,
roles: [{ name: role!.name!, id: role!.id! }],
});

addAlert(t("userCreated"));
navigate(
toUser({ id: createdUser.id, realm: realmName, tab: "settings" }),
);
} catch (error) {
if (isUserProfileError(error)) {
setUserProfileServerError(error, form.setError, ((key, param) =>
t(key as string, param as any)) as TFunction);
} else {
addError("userCreateError", error);
}
}
};

if (!realm || !userProfileMetadata) {
return <KeycloakSpinner />;
}

return (
<>
<ViewHeader titleKey={t("createUser")} />
<PageSection variant="light">
<FormAccess
isHorizontal
onSubmit={handleSubmit(save)}
role="query-users"
>
<FormProvider {...form}>
<UserProfileFields
form={form as any}
userProfileMetadata={userProfileMetadata}
supportedLocales={realm.supportedLocales || []}
currentLocale={currentLocale}
t={t}
/>
<ResetPasswordForm />
</FormProvider>
<FixedButtonsGroup
name="admin-user-creation"
saveText={t("create")}
reset={() => navigate(toUsers({ realm: realm.realm! }))}
resetText={t("cancel")}
isSubmit
/>
</FormAccess>
</PageSection>
</>
);
}
34 changes: 18 additions & 16 deletions js/apps/admin-ui/src/user/EditUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
UserProfileMetadata,
} from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
import {
KeycloakSpinner,
isUserProfileError,
setUserProfileServerError,
useAlerts,
Expand All @@ -27,36 +28,35 @@ import { useNavigate } from "react-router-dom";
import { useAdminClient } from "../admin-client";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { KeyValueType } from "../components/key-value-form/key-value-convert";
import { KeycloakSpinner } from "@keycloak/keycloak-ui-shared";
import {
RoutableTabs,
useRoutableTab,
} from "../components/routable-tabs/RoutableTabs";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { useAccess } from "../context/access/Access";
import { useRealm } from "../context/realm-context/RealmContext";
import { UserEvents } from "../events/UserEvents";
import { UserProfileProvider } from "../realm-settings/user-profile/UserProfileContext";
import useIsFeatureEnabled, { Feature } from "../utils/useIsFeatureEnabled";
import { useParams } from "../utils/useParams";
import { Organizations } from "./Organizations";
import { UserAttributes } from "./UserAttributes";
import { UserConsents } from "./UserConsents";
import { UserCredentials } from "./UserCredentials";
import { BruteForced, UserForm } from "./UserForm";
import { UserGroups } from "./UserGroups";
import { UserIdentityProviderLinks } from "./UserIdentityProviderLinks";
import { UserRoleMapping } from "./UserRoleMapping";
import { UserSessions } from "./UserSessions";
import { UserEvents } from "../events/UserEvents";
import {
UIUserRepresentation,
UserFormFields,
filterManagedAttributes,
toUserFormFields,
toUserRepresentation,
} from "./form-state";
import { Organizations } from "./Organizations";
import { UserParams, UserTab, toUser } from "./routes/User";
import { toUsers } from "./routes/Users";
import { UserAttributes } from "./UserAttributes";
import { UserConsents } from "./UserConsents";
import { UserCredentials } from "./UserCredentials";
import { BruteForced, UserForm } from "./UserForm";
import { UserGroups } from "./UserGroups";
import { UserIdentityProviderLinks } from "./UserIdentityProviderLinks";
import { UserRoleMapping } from "./UserRoleMapping";
import { UserSessions } from "./UserSessions";
import { isLightweightUser } from "./utils";

import "./user-section.css";
Expand Down Expand Up @@ -142,17 +142,19 @@ export default function EditUser() {

const { userProfileMetadata, ...user } = userData;
setUserProfileMetadata(userProfileMetadata);
user.unmanagedAttributes = unmanagedAttributes;
user.attributes = filterManagedAttributes(
user.attributes,
setUser({
...user,
unmanagedAttributes,
);
attributes: filterManagedAttributes(
user.attributes,
unmanagedAttributes,
),
});

if (upConfig.unmanagedAttributePolicy !== undefined) {
setUnmanagedAttributesEnabled(true);
}

setUser(user);
setUpConfig(upConfig);

const isBruteForceProtected = realm.bruteForceProtected;
Expand Down
3 changes: 2 additions & 1 deletion js/apps/admin-ui/src/user/routes.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { AppRouteObject } from "../routes";
import { AddUserRoute } from "./routes/AddUser";
import { AddAdminUserRoute, AddUserRoute } from "./routes/AddUser";
import { UserRoute } from "./routes/User";
import { UsersRoute, UsersRouteWithTab } from "./routes/Users";

const routes: AppRouteObject[] = [
AddUserRoute,
AddAdminUserRoute,
UsersRoute,
UsersRouteWithTab,
UserRoute,
Expand Down
14 changes: 14 additions & 0 deletions js/apps/admin-ui/src/user/routes/AddUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { AppRouteObject } from "../../routes";
export type AddUserParams = { realm: string };

const CreateUser = lazy(() => import("../CreateUser"));
const CreateAdminUser = lazy(() => import("../CreateAdminUser"));

export const AddUserRoute: AppRouteObject = {
path: "/:realm/users/add-user",
Expand All @@ -17,6 +18,19 @@ export const AddUserRoute: AppRouteObject = {
},
};

export const AddAdminUserRoute: AppRouteObject = {
path: "/:realm/users/add-admin-user",
element: <CreateAdminUser />,
breadcrumb: (t) => t("createUser"),
handle: {
access: ["query-users"],
},
};

export const toAddAdminUser = (params: AddUserParams): Partial<Path> => ({
pathname: generateEncodedPath(AddAdminUserRoute.path, params),
});

export const toAddUser = (params: AddUserParams): Partial<Path> => ({
pathname: generateEncodedPath(AddUserRoute.path, params),
});
Loading

0 comments on commit 54aa143

Please sign in to comment.