added organizations table to account (#32311)
* added organizations table to account Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com> Co-authored-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
d63c0fbd13
commit
776a491989
16 changed files with 385 additions and 43 deletions
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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.account;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class OrganizationRepresentation {
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
private String alias;
|
||||
private boolean enabled = true;
|
||||
private String description;
|
||||
private Set<String> domains;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getAlias() {
|
||||
return alias;
|
||||
}
|
||||
|
||||
public void setAlias(String alias) {
|
||||
this.alias = alias;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public Set<String> getDomains() {
|
||||
return domains;
|
||||
}
|
||||
|
||||
public void setDomains(Set<String> domains) {
|
||||
this.domains = domains;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null) return false;
|
||||
if (!(o instanceof OrganizationRepresentation)) return false;
|
||||
|
||||
OrganizationRepresentation that = (OrganizationRepresentation) o;
|
||||
|
||||
return id != null && id.equals(that.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
if (id == null) {
|
||||
return super.hashCode();
|
||||
}
|
||||
return id.hashCode();
|
||||
}
|
||||
}
|
|
@ -128,6 +128,7 @@
|
|||
"isInternationalizationEnabled": ${realm.isInternationalizationEnabled()?c},
|
||||
"isLinkedAccountsEnabled": ${realm.identityFederationEnabled?c},
|
||||
"isMyResourcesEnabled": ${(realm.userManagedAccessAllowed && isAuthorizationEnabled)?c},
|
||||
"isViewOrganizationsEnabled": ${isViewOrganizationsEnabled?c},
|
||||
"deleteAccountAllowed": ${deleteAccountAllowed?c},
|
||||
"updateEmailFeatureEnabled": ${updateEmailFeatureEnabled?c},
|
||||
"updateEmailActionEnabled": ${updateEmailActionEnabled?c},
|
||||
|
|
|
@ -205,4 +205,12 @@ addressScopeConsentText=Address
|
|||
phoneScopeConsentText=Phone number
|
||||
offlineAccessScopeConsentText=Offline Access
|
||||
samlRoleListScopeConsentText=My Roles
|
||||
rolesScopeConsentText=User roles
|
||||
rolesScopeConsentText=User roles
|
||||
organizations=Organizations
|
||||
organizationDescription=View organizations that you joined
|
||||
emptyUserOrganizations=No organizations
|
||||
emptyUserOrganizationsInstructions=You have not joined any organizations yet.
|
||||
searchOrganization=Search for organization
|
||||
organizationList=List of organizations
|
||||
domains=Domains
|
||||
refresh=Refresh
|
|
@ -18,6 +18,11 @@
|
|||
"path": "groups",
|
||||
"isVisible": "isViewGroupsEnabled"
|
||||
},
|
||||
{
|
||||
"label": "organizations",
|
||||
"path": "organizations",
|
||||
"isVisible": "isViewOrganizationsEnabled"
|
||||
},
|
||||
{
|
||||
"label": "resources",
|
||||
"path": "resources",
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
type KeycloakContext,
|
||||
} from "@keycloak/keycloak-ui-shared";
|
||||
|
||||
import OrganizationRepresentation from "@keycloak/keycloak-admin-client/lib/defs/organizationRepresentation";
|
||||
import { joinPath } from "../utils/joinPath";
|
||||
import { parseResponse } from "./parse-response";
|
||||
import {
|
||||
|
@ -156,3 +157,8 @@ export async function getGroups({ signal, context }: CallOptions) {
|
|||
});
|
||||
return parseResponse<Group[]>(response);
|
||||
}
|
||||
|
||||
export async function getUserOrganizations({ signal, context }: CallOptions) {
|
||||
const response = await request("/organizations", context, { signal });
|
||||
return parseResponse<OrganizationRepresentation[]>(response);
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ export type Feature = {
|
|||
updateEmailFeatureEnabled: boolean;
|
||||
updateEmailActionEnabled: boolean;
|
||||
isViewGroupsEnabled: boolean;
|
||||
isViewOrganizationsEnabled: boolean;
|
||||
isOid4VciEnabled: boolean;
|
||||
};
|
||||
|
||||
|
|
48
js/apps/account-ui/src/organizations/Organizations.tsx
Normal file
48
js/apps/account-ui/src/organizations/Organizations.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import OrganizationRepresentation from "@keycloak/keycloak-admin-client/lib/defs/organizationRepresentation";
|
||||
import {
|
||||
ErrorBoundaryProvider,
|
||||
KeycloakSpinner,
|
||||
ListEmptyState,
|
||||
OrganizationTable,
|
||||
useEnvironment,
|
||||
} from "@keycloak/keycloak-ui-shared";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getUserOrganizations } from "../api/methods";
|
||||
import { Page } from "../components/page/Page";
|
||||
import { Environment } from "../environment";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
|
||||
export const Organizations = () => {
|
||||
const { t } = useTranslation();
|
||||
const context = useEnvironment<Environment>();
|
||||
|
||||
const [userOrgs, setUserOrgs] = useState<OrganizationRepresentation[]>([]);
|
||||
|
||||
usePromise(
|
||||
(signal) => getUserOrganizations({ signal, context }),
|
||||
setUserOrgs,
|
||||
);
|
||||
|
||||
if (!userOrgs) {
|
||||
return <KeycloakSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page title={t("organizations")} description={t("organizationDescription")}>
|
||||
<ErrorBoundaryProvider>
|
||||
<OrganizationTable
|
||||
link={({ children }) => <span>{children}</span>}
|
||||
loader={userOrgs}
|
||||
>
|
||||
<ListEmptyState
|
||||
message={t("emptyUserOrganizations")}
|
||||
instructions={t("emptyUserOrganizationsInstructions")}
|
||||
/>
|
||||
</OrganizationTable>
|
||||
</ErrorBoundaryProvider>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default Organizations;
|
|
@ -2,6 +2,7 @@ import { lazy } from "react";
|
|||
import type { IndexRouteObject, RouteObject } from "react-router-dom";
|
||||
|
||||
import { environment } from "./environment";
|
||||
import { Organizations } from "./organizations/Organizations";
|
||||
import { ErrorPage } from "./root/ErrorPage";
|
||||
import { Root } from "./root/Root";
|
||||
|
||||
|
@ -59,6 +60,11 @@ export const PersonalInfoRoute: IndexRouteObject = {
|
|||
element: <PersonalInfo />,
|
||||
};
|
||||
|
||||
export const OrganizationsRoute: RouteObject = {
|
||||
path: "organizations",
|
||||
element: <Organizations />,
|
||||
};
|
||||
|
||||
export const Oid4VciRoute: RouteObject = {
|
||||
path: "oid4vci",
|
||||
element: <Oid4Vci />,
|
||||
|
@ -75,6 +81,7 @@ export const RootRoute: RouteObject = {
|
|||
SigningInRoute,
|
||||
ApplicationsRoute,
|
||||
GroupsRoute,
|
||||
OrganizationsRoute,
|
||||
PersonalInfoRoute,
|
||||
ResourcesRoute,
|
||||
ContentRoute,
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import OrganizationRepresentation from "@keycloak/keycloak-admin-client/lib/defs/organizationRepresentation";
|
||||
import { useAlerts } from "@keycloak/keycloak-ui-shared";
|
||||
import {
|
||||
ListEmptyState,
|
||||
OrganizationTable,
|
||||
useAlerts,
|
||||
} from "@keycloak/keycloak-ui-shared";
|
||||
import {
|
||||
Button,
|
||||
ButtonVariant,
|
||||
|
@ -11,10 +15,9 @@ import { useTranslation } from "react-i18next";
|
|||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useAdminClient } from "../admin-client";
|
||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||
import { ListEmptyState } from "@keycloak/keycloak-ui-shared";
|
||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
import { OrganizationTable } from "./OrganizationTable";
|
||||
import { toEditOrganization } from "../organizations/routes/EditOrganization";
|
||||
import { toAddOrganization } from "./routes/AddOrganization";
|
||||
|
||||
export default function OrganizationSection() {
|
||||
|
@ -61,6 +64,18 @@ export default function OrganizationSection() {
|
|||
<PageSection variant="light" className="pf-v5-u-p-0">
|
||||
<DeleteConfirm />
|
||||
<OrganizationTable
|
||||
link={({ organization, children }) => (
|
||||
<Link
|
||||
key={organization.id}
|
||||
to={toEditOrganization({
|
||||
realm,
|
||||
id: organization.id!,
|
||||
tab: "settings",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)}
|
||||
key={key}
|
||||
loader={loader}
|
||||
isPaginated
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import OrganizationRepresentation from "@keycloak/keycloak-admin-client/lib/defs/organizationRepresentation";
|
||||
import {
|
||||
ListEmptyState,
|
||||
OrganizationTable,
|
||||
useAlerts,
|
||||
useFetch,
|
||||
} from "@keycloak/keycloak-ui-shared";
|
||||
|
@ -15,11 +16,12 @@ import {
|
|||
} from "@patternfly/react-core";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { useAdminClient } from "../admin-client";
|
||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
import { OrganizationModal } from "../organizations/OrganizationModal";
|
||||
import { OrganizationTable } from "../organizations/OrganizationTable";
|
||||
import { toEditOrganization } from "../organizations/routes/EditOrganization";
|
||||
import useToggle from "../utils/useToggle";
|
||||
import { UserParams } from "./routes/User";
|
||||
|
||||
|
@ -28,6 +30,7 @@ export const Organizations = () => {
|
|||
const { t } = useTranslation();
|
||||
const { id } = useParams<UserParams>();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const { realm } = useRealm();
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(key + 1);
|
||||
|
@ -116,6 +119,18 @@ export const Organizations = () => {
|
|||
)}
|
||||
<DeleteConfirm />
|
||||
<OrganizationTable
|
||||
link={({ organization, children }) => (
|
||||
<Link
|
||||
key={organization.id}
|
||||
to={toEditOrganization({
|
||||
realm,
|
||||
id: organization.id!,
|
||||
tab: "settings",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)}
|
||||
loader={userOrgs}
|
||||
onSelect={(orgs) => setSelectedOrgs(orgs)}
|
||||
deleteLabel="remove"
|
||||
|
|
|
@ -1,29 +1,23 @@
|
|||
import OrganizationRepresentation from "@keycloak/keycloak-admin-client/lib/defs/organizationRepresentation";
|
||||
import { Badge, Chip, ChipGroup } from "@patternfly/react-core";
|
||||
import { TableText } from "@patternfly/react-table";
|
||||
import { PropsWithChildren, ReactNode } from "react";
|
||||
import { FunctionComponent, PropsWithChildren, ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
KeycloakDataTable,
|
||||
LoaderFunction,
|
||||
} from "@keycloak/keycloak-ui-shared";
|
||||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
import { toEditOrganization } from "./routes/EditOrganization";
|
||||
import { KeycloakDataTable, LoaderFunction } from "./table/KeycloakDataTable";
|
||||
|
||||
const OrgDetailLink = (organization: OrganizationRepresentation) => {
|
||||
type OrgDetailLinkProps = {
|
||||
link: FunctionComponent<
|
||||
PropsWithChildren<{ organization: OrganizationRepresentation }>
|
||||
>;
|
||||
organization: OrganizationRepresentation;
|
||||
};
|
||||
|
||||
const OrgDetailLink = ({ link, organization }: OrgDetailLinkProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { realm } = useRealm();
|
||||
const Component = link;
|
||||
return (
|
||||
<TableText wrapModifier="truncate">
|
||||
<Link
|
||||
key={organization.id}
|
||||
to={toEditOrganization({
|
||||
realm,
|
||||
id: organization.id!,
|
||||
tab: "settings",
|
||||
})}
|
||||
>
|
||||
<Component organization={organization}>
|
||||
{organization.name}
|
||||
{!organization.enabled && (
|
||||
<Badge
|
||||
|
@ -34,7 +28,7 @@ const OrgDetailLink = (organization: OrganizationRepresentation) => {
|
|||
{t("disabled")}
|
||||
</Badge>
|
||||
)}
|
||||
</Link>
|
||||
</Component>
|
||||
</TableText>
|
||||
);
|
||||
};
|
||||
|
@ -47,11 +41,14 @@ const Domains = (org: OrganizationRepresentation) => {
|
|||
expandedText={t("hide")}
|
||||
collapsedText={t("showRemaining")}
|
||||
>
|
||||
{org.domains?.map((dn) => (
|
||||
<Chip key={dn.name} isReadOnly>
|
||||
{dn.name}
|
||||
</Chip>
|
||||
))}
|
||||
{org.domains?.map((dn) => {
|
||||
const name = typeof dn === "string" ? dn : dn.name;
|
||||
return (
|
||||
<Chip key={name} isReadOnly>
|
||||
{name}
|
||||
</Chip>
|
||||
);
|
||||
})}
|
||||
</ChipGroup>
|
||||
);
|
||||
};
|
||||
|
@ -60,6 +57,9 @@ type OrganizationTableProps = PropsWithChildren & {
|
|||
loader:
|
||||
| LoaderFunction<OrganizationRepresentation>
|
||||
| OrganizationRepresentation[];
|
||||
link: FunctionComponent<
|
||||
PropsWithChildren<{ organization: OrganizationRepresentation }>
|
||||
>;
|
||||
toolbarItem?: ReactNode;
|
||||
isPaginated?: boolean;
|
||||
onSelect?: (orgs: OrganizationRepresentation[]) => void;
|
||||
|
@ -74,6 +74,7 @@ export const OrganizationTable = ({
|
|||
onSelect,
|
||||
onDelete,
|
||||
deleteLabel = "delete",
|
||||
link,
|
||||
children,
|
||||
}: OrganizationTableProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
@ -87,17 +88,23 @@ export const OrganizationTable = ({
|
|||
toolbarItem={toolbarItem}
|
||||
onSelect={onSelect}
|
||||
canSelectAll={onSelect !== undefined}
|
||||
actions={[
|
||||
{
|
||||
title: t(deleteLabel),
|
||||
onRowClick: onDelete,
|
||||
},
|
||||
]}
|
||||
actions={
|
||||
onDelete
|
||||
? [
|
||||
{
|
||||
title: t(deleteLabel),
|
||||
onRowClick: onDelete,
|
||||
},
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
columns={[
|
||||
{
|
||||
name: "name",
|
||||
displayKey: "name",
|
||||
cellRenderer: OrgDetailLink,
|
||||
cellRenderer: (row) => (
|
||||
<OrgDetailLink link={link} organization={row} />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "domains",
|
|
@ -92,3 +92,4 @@ export {
|
|||
ErrorBoundaryProvider,
|
||||
} from "./utils/ErrorBoundary";
|
||||
export type { FallbackProps } from "./utils/ErrorBoundary";
|
||||
export { OrganizationTable } from "./controls/OrganizationTable";
|
||||
|
|
|
@ -160,6 +160,7 @@ public class AccountConsole implements AccountResourceProvider {
|
|||
map.put("deleteAccountAllowed", deleteAccountAllowed);
|
||||
|
||||
map.put("isViewGroupsEnabled", isViewGroupsEnabled);
|
||||
map.put("isViewOrganizationsEnabled", Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION));
|
||||
map.put("isOid4VciEnabled", Profile.isFeatureEnabled(Profile.Feature.OID4VC_VCI));
|
||||
|
||||
map.put("updateEmailFeatureEnabled", Profile.isFeatureEnabled(Profile.Feature.UPDATE_EMAIL));
|
||||
|
|
|
@ -46,6 +46,7 @@ import jakarta.ws.rs.core.MediaType;
|
|||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.jboss.resteasy.reactive.NoCache;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
import org.keycloak.http.HttpRequest;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.common.Profile;
|
||||
|
@ -100,7 +101,7 @@ public class AccountRestService {
|
|||
private final KeycloakSession session;
|
||||
private final EventBuilder event;
|
||||
private final Auth auth;
|
||||
|
||||
|
||||
private final RealmModel realm;
|
||||
private final UserModel user;
|
||||
private final Locale locale;
|
||||
|
@ -119,7 +120,7 @@ public class AccountRestService {
|
|||
this.request = session.getContext().getHttpRequest();
|
||||
this.headers = session.getContext().getRequestHeaders();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get account information.
|
||||
*
|
||||
|
@ -187,7 +188,7 @@ public class AccountRestService {
|
|||
AttributeMetadata am = userProfileAttributes.getMetadata(p.toString());
|
||||
if(am != null)
|
||||
ret[i++] = am.getAttributeDisplayName();
|
||||
else
|
||||
else
|
||||
ret[i++] = p.toString();
|
||||
} else {
|
||||
ret[i++] = p.toString();
|
||||
|
@ -230,6 +231,16 @@ public class AccountRestService {
|
|||
return auth.getRealm().getSupportedLocalesStream().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Path("/organizations")
|
||||
public OrganizationsResource organizations() {
|
||||
checkAccountApiEnabled();
|
||||
if (!Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
||||
return new OrganizationsResource(session, auth, user);
|
||||
}
|
||||
|
||||
private ClientRepresentation modelToRepresentation(ClientModel model, List<String> inUseClients, List<String> offlineClients, Map<String, UserConsentModel> consents) {
|
||||
ClientRepresentation representation = new ClientRepresentation();
|
||||
representation.setClientId(model.getClientId());
|
||||
|
@ -420,7 +431,7 @@ public class AccountRestService {
|
|||
}
|
||||
return consent;
|
||||
}
|
||||
|
||||
|
||||
@Path("/linked-accounts")
|
||||
public LinkedAccountsResource linkedAccounts() {
|
||||
return new LinkedAccountsResource(session, request, auth, event, user);
|
||||
|
@ -482,10 +493,10 @@ public class AccountRestService {
|
|||
}
|
||||
|
||||
// TODO Logs
|
||||
|
||||
|
||||
private static void checkAccountApiEnabled() {
|
||||
if (!Profile.isFeatureEnabled(Profile.Feature.ACCOUNT_API)) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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.resources.account;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.keycloak.models.AccountRoles;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.OrganizationDomainModel;
|
||||
import org.keycloak.models.OrganizationModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.organization.OrganizationProvider;
|
||||
import org.keycloak.representations.account.OrganizationRepresentation;
|
||||
import org.keycloak.services.cors.Cors;
|
||||
import org.keycloak.services.managers.Auth;
|
||||
|
||||
public class OrganizationsResource {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final UserModel user;
|
||||
private final Auth auth;
|
||||
|
||||
public OrganizationsResource(KeycloakSession session,
|
||||
Auth auth,
|
||||
UserModel user) {
|
||||
this.session = session;
|
||||
this.auth = auth;
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response getOrganizations() {
|
||||
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
||||
return Cors.builder().auth()
|
||||
.allowedOrigins(auth.getToken())
|
||||
.add(Response.ok(session.getProvider(OrganizationProvider.class)
|
||||
.getByMember(user)
|
||||
.map(this::toRepresentation))
|
||||
);
|
||||
}
|
||||
|
||||
private OrganizationRepresentation toRepresentation(OrganizationModel model) {
|
||||
OrganizationRepresentation rep = new OrganizationRepresentation();
|
||||
|
||||
rep.setId(model.getId());
|
||||
rep.setName(model.getName());
|
||||
rep.setAlias(model.getAlias());
|
||||
rep.setDescription(model.getDescription());
|
||||
rep.setEnabled(model.isEnabled());
|
||||
rep.setDomains(model.getDomains().map(OrganizationDomainModel::getName).collect(Collectors.toSet()));
|
||||
|
||||
return rep;
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@
|
|||
package org.keycloak.testsuite.organization.account;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.SortedSet;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
|
@ -33,13 +34,16 @@ import org.keycloak.admin.client.resource.OrganizationResource;
|
|||
import org.keycloak.broker.provider.util.SimpleHttp.Response;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
import org.keycloak.representations.account.LinkedAccountRepresentation;
|
||||
import org.keycloak.representations.account.OrganizationRepresentation;
|
||||
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.broker.util.SimpleHttpDefault;
|
||||
import org.keycloak.testsuite.organization.admin.AbstractOrganizationTest;
|
||||
import org.keycloak.testsuite.util.TokenUtil;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
|
||||
@EnableFeature(Feature.ORGANIZATION)
|
||||
public class OrganizationAccountTest extends AbstractOrganizationTest {
|
||||
|
@ -87,6 +91,29 @@ public class OrganizationAccountTest extends AbstractOrganizationTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetOrganizations() throws Exception {
|
||||
UserRepresentation member = createUser();
|
||||
org.keycloak.representations.idm.OrganizationRepresentation orgA = createOrganization("orga");
|
||||
testRealm().organizations().get(orgA.getId()).members().addMember(member.getId()).close();
|
||||
org.keycloak.representations.idm.OrganizationRepresentation orgB = createOrganization("orgb");
|
||||
testRealm().organizations().get(orgB.getId()).members().addMember(member.getId()).close();
|
||||
|
||||
List<OrganizationRepresentation> organizations = getOrganizations();
|
||||
Assert.assertEquals(2, organizations.size());
|
||||
OrganizationRepresentation organization = organizations.stream()
|
||||
.filter(o -> orgA.getId().equals(o.getId()))
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
Assert.assertNotNull(organization);
|
||||
Assert.assertEquals(orgA.getId(), organization.getId());
|
||||
Assert.assertEquals(orgA.getAlias(), organization.getAlias());
|
||||
Assert.assertEquals(orgA.getName(), organization.getName());
|
||||
Assert.assertEquals(orgA.getDescription(), organization.getDescription());
|
||||
Assert.assertEquals(orgA.getDomains().size(), organization.getDomains().size());
|
||||
Assert.assertTrue(organization.getDomains().containsAll(orgA.getDomains().stream().map(OrganizationDomainRepresentation::getName).toList()));
|
||||
}
|
||||
|
||||
private SortedSet<LinkedAccountRepresentation> linkedAccountsRep() throws IOException {
|
||||
return SimpleHttpDefault.doGet(getAccountUrl("linked-accounts"), client).auth(tokenUtil.getToken())
|
||||
.asJson(new TypeReference<>() {});
|
||||
|
@ -103,4 +130,21 @@ public class OrganizationAccountTest extends AbstractOrganizationTest {
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<OrganizationRepresentation> getOrganizations() throws IOException {
|
||||
return SimpleHttpDefault.doGet(getAccountUrl("organizations"), client).auth(tokenUtil.getToken())
|
||||
.asJson(new TypeReference<>() {});
|
||||
}
|
||||
|
||||
private UserRepresentation createUser() {
|
||||
testRealm().users().create(UserBuilder.create()
|
||||
.username(bc.getUserEmail())
|
||||
.email(bc.getUserEmail())
|
||||
.password(bc.getUserPassword())
|
||||
.enabled(true)
|
||||
.build()).close();
|
||||
UserRepresentation member = testRealm().users().searchByEmail(bc.getUserEmail(), true).get(0);
|
||||
getCleanup().addUserId(member.getId());
|
||||
return member;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue