feat(auth): add useIsEdgeAdmin hook [EE-6627] (#11101)

pull/11189/head
Chaim Lev-Ari 2024-02-15 00:50:26 +02:00 committed by GitHub
parent c08b5af85a
commit edea9e3481
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 389 additions and 198 deletions

View File

@ -64,7 +64,7 @@ angular.module('portainer.app').controller('porAccessControlFormController', [
this.$onInit = $onInit; this.$onInit = $onInit;
function $onInit() { function $onInit() {
var isAdmin = Authentication.isAdmin(); var isAdmin = Authentication.isPureAdmin();
ctrl.isAdmin = isAdmin; ctrl.isAdmin = isAdmin;
if (isAdmin) { if (isAdmin) {

View File

@ -1,4 +1,5 @@
import { getCurrentUser } from '../users/queries/useLoadCurrentUser'; import { getCurrentUser } from '../users/queries/useLoadCurrentUser';
import * as userHelpers from '../users/user.helpers';
import { clear as clearSessionStorage } from './session-storage'; import { clear as clearSessionStorage } from './session-storage';
const DEFAULT_USER = 'admin'; const DEFAULT_USER = 'admin';
@ -25,6 +26,9 @@ angular.module('portainer.app').factory('Authentication', [
service.isAuthenticated = isAuthenticated; service.isAuthenticated = isAuthenticated;
service.getUserDetails = getUserDetails; service.getUserDetails = getUserDetails;
service.isAdmin = isAdmin; service.isAdmin = isAdmin;
service.isEdgeAdmin = isEdgeAdmin;
service.isPureAdmin = isPureAdmin;
service.hasAuthorizations = hasAuthorizations;
async function initAsync() { async function initAsync() {
try { try {
@ -120,8 +124,36 @@ angular.module('portainer.app').factory('Authentication', [
return login(DEFAULT_USER, DEFAULT_PASSWORD); return login(DEFAULT_USER, DEFAULT_PASSWORD);
} }
// To avoid creating divergence between CE and EE
// isAdmin checks if the user is a portainer admin or edge admin
function isEdgeAdmin() {
const environment = EndpointProvider.currentEndpoint();
return userHelpers.isEdgeAdmin({ Role: user.role }, environment);
}
/**
* @deprecated use Authentication.isAdmin instead
*/
function isAdmin() { function isAdmin() {
return !!user && user.role === 1; return isEdgeAdmin();
}
// To avoid creating divergence between CE and EE
// isPureAdmin checks if the user is portainer admin only
function isPureAdmin() {
return userHelpers.isPureAdmin({ Role: user.role });
}
function hasAuthorizations(authorizations) {
const endpointId = EndpointProvider.endpointID();
if (isAdmin()) {
return true;
}
if (!user.endpointAuthorizations || !user.endpointAuthorizations[endpointId]) {
return false;
}
const userEndpointAuthorizations = user.endpointAuthorizations[endpointId];
return authorizations.some((authorization) => userEndpointAuthorizations[authorization]);
} }
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {

View File

@ -1,9 +1,9 @@
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { TeamRole, TeamMembership } from '@/react/portainer/users/teams/types'; import { TeamRole, TeamMembership } from '@/react/portainer/users/teams/types';
import { useCurrentUser, useIsEdgeAdmin } from '@/react/hooks/useUser';
import { User, UserId } from './types'; import { User, UserId } from './types';
import { isAdmin } from './user.helpers';
import { getUserMemberships, getUsers } from './user.service'; import { getUserMemberships, getUsers } from './user.service';
interface UseUserMembershipOptions<TSelect> { interface UseUserMembershipOptions<TSelect> {
@ -22,14 +22,21 @@ export function useUserMembership<TSelect = TeamMembership[]>(
); );
} }
export function useIsTeamLeader(user: User) { export function useIsCurrentUserTeamLeader() {
const { user } = useCurrentUser();
const isAdminQuery = useIsEdgeAdmin();
const query = useUserMembership(user.Id, { const query = useUserMembership(user.Id, {
enabled: !isAdmin(user), enabled: !isAdminQuery.isLoading && !isAdminQuery.isAdmin,
select: (memberships) => select: (memberships) =>
memberships.some((membership) => membership.Role === TeamRole.Leader), memberships.some((membership) => membership.Role === TeamRole.Leader),
}); });
return isAdmin(user) ? true : query.data; if (isAdminQuery.isLoading) {
return false;
}
return isAdminQuery.isAdmin ? true : !!query.data;
} }
export function useUsers<T = User[]>( export function useUsers<T = User[]>(

View File

@ -7,6 +7,7 @@ export { type UserId };
export enum Role { export enum Role {
Admin = 1, Admin = 1,
Standard, Standard,
EdgeAdmin,
} }
interface AuthorizationMap { interface AuthorizationMap {

View File

@ -1,9 +1,30 @@
import { Environment } from '@/react/portainer/environments/types';
import { isEdgeEnvironment } from '@/react/portainer/environments/utils';
import { Role, User } from './types'; import { Role, User } from './types';
export function filterNonAdministratorUsers(users: User[]) { export function filterNonAdministratorUsers(users: User[]) {
return users.filter((user) => user.Role !== Role.Admin); return users.filter((user) => user.Role !== Role.Admin);
} }
export function isAdmin(user?: User): boolean { type UserLike = Pick<User, 'Role'>;
return !!user && user.Role === 1;
// To avoid creating divergence between CE and EE
// isAdmin checks if the user is portainer admin or edge admin
export function isEdgeAdmin(
user: UserLike | undefined,
environment?: Pick<Environment, 'Type'> | null
): boolean {
return (
isPureAdmin(user) ||
(user?.Role === Role.EdgeAdmin &&
(!environment || isEdgeEnvironment(environment.Type)))
);
}
// To avoid creating divergence between CE and EE
// isPureAdmin checks only if the user is portainer admin
// See bouncer.IsAdmin and bouncer.PureAdminAccess
export function isPureAdmin(user?: UserLike): boolean {
return !!user && user.Role === Role.Admin;
} }

View File

@ -4,7 +4,7 @@ import { Plus } from 'lucide-react';
import { ContainerInstanceFormValues } from '@/react/azure/types'; import { ContainerInstanceFormValues } from '@/react/azure/types';
import * as notifications from '@/portainer/services/notifications'; import * as notifications from '@/portainer/services/notifications';
import { useUser } from '@/react/hooks/useUser'; import { useCurrentUser } from '@/react/hooks/useUser';
import { AccessControlForm } from '@/react/portainer/access-control/AccessControlForm'; import { AccessControlForm } from '@/react/portainer/access-control/AccessControlForm';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
@ -24,7 +24,7 @@ import { useCreateInstanceMutation } from './useCreateInstanceMutation';
export function CreateContainerInstanceForm() { export function CreateContainerInstanceForm() {
const environmentId = useEnvironmentId(); const environmentId = useEnvironmentId();
const { isAdmin } = useUser(); const { isPureAdmin } = useCurrentUser();
const { providers, subscriptions, resourceGroups, isLoading } = const { providers, subscriptions, resourceGroups, isLoading } =
useLoadFormState(environmentId); useLoadFormState(environmentId);
@ -49,7 +49,7 @@ export function CreateContainerInstanceForm() {
return ( return (
<Formik<ContainerInstanceFormValues> <Formik<ContainerInstanceFormValues>
initialValues={initialValues} initialValues={initialValues}
validationSchema={() => validationSchema(isAdmin)} validationSchema={() => validationSchema(isPureAdmin)}
onSubmit={onSubmit} onSubmit={onSubmit}
validateOnMount validateOnMount
validateOnChange validateOnChange

View File

@ -37,7 +37,7 @@ export function useFormState(
resourceGroups: Record<string, ResourceGroup[]> = {}, resourceGroups: Record<string, ResourceGroup[]> = {},
providers: Record<string, ProviderViewModel> = {} providers: Record<string, ProviderViewModel> = {}
) { ) {
const { isAdmin, user } = useCurrentUser(); const { user, isPureAdmin } = useCurrentUser();
const subscriptionOptions = subscriptions.map((s) => ({ const subscriptionOptions = subscriptions.map((s) => ({
value: s.subscriptionId, value: s.subscriptionId,
@ -67,7 +67,7 @@ export function useFormState(
cpu: 1, cpu: 1,
ports: [{ container: 80, host: 80, protocol: 'TCP' }], ports: [{ container: 80, host: 80, protocol: 'TCP' }],
allocatePublicIP: true, allocatePublicIP: true,
accessControl: parseAccessControlFormData(isAdmin, user.Id), accessControl: parseAccessControlFormData(isPureAdmin, user.Id),
}; };
return { return {

View File

@ -66,7 +66,7 @@ function RateLimitsInner({
environment: Environment; environment: Environment;
}) { }) {
const pullRateLimits = useRateLimits(registryId, environment, onRateLimit); const pullRateLimits = useRateLimits(registryId, environment, onRateLimit);
const { isAdmin } = useCurrentUser(); const { isPureAdmin } = useCurrentUser();
if (!pullRateLimits) { if (!pullRateLimits) {
return null; return null;
@ -88,7 +88,7 @@ function RateLimitsInner({
</> </>
) : ( ) : (
<> <>
{isAdmin ? ( {isPureAdmin ? (
<> <>
You are currently using an anonymous account to pull images You are currently using an anonymous account to pull images
from DockerHub and will be limited to 100 pulls every 6 from DockerHub and will be limited to 100 pulls every 6

View File

@ -10,7 +10,7 @@ import { Values } from './BaseForm';
export function toViewModel( export function toViewModel(
config: ContainerResponse, config: ContainerResponse,
isAdmin: boolean, isPureAdmin: boolean,
currentUserId: UserId, currentUserId: UserId,
nodeName: string, nodeName: string,
image: Values['image'], image: Values['image'],
@ -18,7 +18,7 @@ export function toViewModel(
): Values { ): Values {
// accessControl shouldn't be copied to new container // accessControl shouldn't be copied to new container
const accessControl = parseAccessControlFormData(isAdmin, currentUserId); const accessControl = parseAccessControlFormData(isPureAdmin, currentUserId);
if (config.Portainer?.ResourceControl?.Public) { if (config.Portainer?.ResourceControl?.Public) {
accessControl.ownership = ResourceControlOwnership.PUBLIC; accessControl.ownership = ResourceControlOwnership.PUBLIC;
@ -38,11 +38,11 @@ export function toViewModel(
} }
export function getDefaultViewModel( export function getDefaultViewModel(
isAdmin: boolean, isPureAdmin: boolean,
currentUserId: UserId, currentUserId: UserId,
nodeName: string nodeName: string
): Values { ): Values {
const accessControl = parseAccessControlFormData(isAdmin, currentUserId); const accessControl = parseAccessControlFormData(isPureAdmin, currentUserId);
return { return {
nodeName, nodeName,

View File

@ -2,7 +2,7 @@ import { Formik } from 'formik';
import { useRouter } from '@uirouter/react'; import { useRouter } from '@uirouter/react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useCurrentUser, useIsEnvironmentAdmin } from '@/react/hooks/useUser'; import { useIsEdgeAdmin, useIsEnvironmentAdmin } from '@/react/hooks/useUser';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment'; import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
import { useEnvironmentRegistries } from '@/react/portainer/environments/queries/useEnvironmentRegistries'; import { useEnvironmentRegistries } from '@/react/portainer/environments/queries/useEnvironmentRegistries';
@ -48,7 +48,7 @@ function CreateForm() {
const environmentId = useEnvironmentId(); const environmentId = useEnvironmentId();
const router = useRouter(); const router = useRouter();
const { trackEvent } = useAnalytics(); const { trackEvent } = useAnalytics();
const { isAdmin } = useCurrentUser(); const isAdminQuery = useIsEdgeAdmin();
const isEnvironmentAdmin = useIsEnvironmentAdmin(); const isEnvironmentAdmin = useIsEnvironmentAdmin();
const [isDockerhubRateLimited, setIsDockerhubRateLimited] = useState(false); const [isDockerhubRateLimited, setIsDockerhubRateLimited] = useState(false);
@ -67,7 +67,7 @@ function CreateForm() {
const envQuery = useCurrentEnvironment(); const envQuery = useCurrentEnvironment();
const validationSchema = useValidation({ const validationSchema = useValidation({
isAdmin, isAdmin: isAdminQuery.isAdmin,
maxCpu, maxCpu,
maxMemory, maxMemory,
isDuplicating: initialValuesQuery?.isDuplicating, isDuplicating: initialValuesQuery?.isDuplicating,

View File

@ -102,7 +102,7 @@ export function InnerForm({
} }
errors={errors.volumes} errors={errors.volumes}
allowBindMounts={ allowBindMounts={
isEnvironmentAdmin || isEnvironmentAdmin.authorized ||
environment.SecuritySettings environment.SecuritySettings
.allowBindMountsForRegularUsers .allowBindMountsForRegularUsers
} }
@ -166,18 +166,18 @@ export function InnerForm({
setFieldValue(`resources.${field}`, value) setFieldValue(`resources.${field}`, value)
} }
allowPrivilegedMode={ allowPrivilegedMode={
isEnvironmentAdmin || isEnvironmentAdmin.authorized ||
environment.SecuritySettings environment.SecuritySettings
.allowPrivilegedModeForRegularUsers .allowPrivilegedModeForRegularUsers
} }
isDevicesFieldVisible={ isDevicesFieldVisible={
isEnvironmentAdmin || isEnvironmentAdmin.authorized ||
environment.SecuritySettings environment.SecuritySettings
.allowDeviceMappingForRegularUsers .allowDeviceMappingForRegularUsers
} }
isInitFieldVisible={apiVersion >= 1.37} isInitFieldVisible={apiVersion >= 1.37}
isSysctlFieldVisible={ isSysctlFieldVisible={
isEnvironmentAdmin || isEnvironmentAdmin.authorized ||
environment.SecuritySettings environment.SecuritySettings
.allowSysctlSettingForRegularUsers .allowSysctlSettingForRegularUsers
} }

View File

@ -62,7 +62,8 @@ export function useInitialValues(submitting: boolean) {
params: { nodeName, from }, params: { nodeName, from },
} = useCurrentStateAndParams(); } = useCurrentStateAndParams();
const environmentId = useEnvironmentId(); const environmentId = useEnvironmentId();
const { isAdmin, user } = useCurrentUser(); const { user, isPureAdmin } = useCurrentUser();
const networksQuery = useNetworksForSelector(); const networksQuery = useNetworksForSelector();
const fromContainerQuery = useContainer(environmentId, from, { const fromContainerQuery = useContainer(environmentId, from, {
@ -85,7 +86,7 @@ export function useInitialValues(submitting: boolean) {
if (!from) { if (!from) {
return { return {
initialValues: defaultValues(isAdmin, user.Id, nodeName), initialValues: defaultValues(isPureAdmin, user.Id, nodeName),
}; };
} }
@ -136,7 +137,7 @@ export function useInitialValues(submitting: boolean) {
env: envVarsTabUtils.toViewModel(fromContainer), env: envVarsTabUtils.toViewModel(fromContainer),
...baseFormUtils.toViewModel( ...baseFormUtils.toViewModel(
fromContainer, fromContainer,
isAdmin, isPureAdmin,
user.Id, user.Id,
nodeName, nodeName,
imageConfig, imageConfig,
@ -148,7 +149,7 @@ export function useInitialValues(submitting: boolean) {
} }
function defaultValues( function defaultValues(
isAdmin: boolean, isPureAdmin: boolean,
currentUserId: UserId, currentUserId: UserId,
nodeName: string nodeName: string
): Values { ): Values {
@ -161,6 +162,6 @@ function defaultValues(
resources: resourcesTabUtils.getDefaultViewModel(), resources: resourcesTabUtils.getDefaultViewModel(),
capabilities: capabilitiesTabUtils.getDefaultViewModel(), capabilities: capabilitiesTabUtils.getDefaultViewModel(),
env: envVarsTabUtils.getDefaultViewModel(), env: envVarsTabUtils.getDefaultViewModel(),
...baseFormUtils.getDefaultViewModel(isAdmin, currentUserId, nodeName), ...baseFormUtils.getDefaultViewModel(isPureAdmin, currentUserId, nodeName),
}; };
} }

View File

@ -1,4 +1,4 @@
import { render } from '@/react-tools/test-utils'; import { renderWithQueryClient } from '@/react-tools/test-utils';
import { UserContext } from '@/react/hooks/useUser'; import { UserContext } from '@/react/hooks/useUser';
import { UserViewModel } from '@/portainer/models/user'; import { UserViewModel } from '@/portainer/models/user';
@ -50,7 +50,7 @@ test('Non system networks should have a delete button', async () => {
async function renderComponent(isAdmin: boolean, network: DockerNetwork) { async function renderComponent(isAdmin: boolean, network: DockerNetwork) {
const user = new UserViewModel({ Username: 'test', Role: isAdmin ? 1 : 2 }); const user = new UserViewModel({ Username: 'test', Role: isAdmin ? 1 : 2 });
const queries = render( const queries = renderWithQueryClient(
<UserContext.Provider value={{ user }}> <UserContext.Provider value={{ user }}>
<NetworkDetailsTable <NetworkDetailsTable
network={network} network={network}

View File

@ -1,7 +1,7 @@
import { Layers } from 'lucide-react'; import { Layers } from 'lucide-react';
import { Row } from '@tanstack/react-table'; import { Row } from '@tanstack/react-table';
import { useAuthorizations, useCurrentUser } from '@/react/hooks/useUser'; import { useAuthorizations, useIsEdgeAdmin } from '@/react/hooks/useUser';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { Datatable } from '@@/datatables'; import { Datatable } from '@@/datatables';
@ -34,7 +34,7 @@ export function StacksDatatable({
}) { }) {
const tableState = useTableState(settingsStore, tableKey); const tableState = useTableState(settingsStore, tableKey);
useRepeater(tableState.autoRefreshRate, onReload); useRepeater(tableState.autoRefreshRate, onReload);
const { isAdmin } = useCurrentUser(); const isAdminQuery = useIsEdgeAdmin();
const canManageStacks = useAuthorizations([ const canManageStacks = useAuthorizations([
'PortainerStackCreate', 'PortainerStackCreate',
'PortainerStackDelete', 'PortainerStackDelete',
@ -58,7 +58,7 @@ export function StacksDatatable({
columns={columns} columns={columns}
dataset={dataset} dataset={dataset}
isRowSelectable={({ original: item }) => isRowSelectable={({ original: item }) =>
allowSelection(item, isAdmin, canManageStacks) allowSelection(item, isAdminQuery.isAdmin, canManageStacks.authorized)
} }
getRowId={(item) => item.Id.toString()} getRowId={(item) => item.Id.toString()}
initialTableState={{ initialTableState={{

View File

@ -1,6 +1,6 @@
import { CellContext, Column } from '@tanstack/react-table'; import { CellContext, Column } from '@tanstack/react-table';
import { useCurrentUser } from '@/react/hooks/useUser'; import { useIsEdgeAdmin } from '@/react/hooks/useUser';
import { getValueAsArrayOfStrings } from '@/portainer/helpers/array'; import { getValueAsArrayOfStrings } from '@/portainer/helpers/array';
import { StackStatus } from '@/react/common/stacks/types'; import { StackStatus } from '@/react/common/stacks/types';
import { import {
@ -67,7 +67,7 @@ function NameCell({
} }
function NameLink({ item }: { item: DecoratedStack }) { function NameLink({ item }: { item: DecoratedStack }) {
const { isAdmin } = useCurrentUser(); const isAdminQuery = useIsEdgeAdmin();
const name = item.Name; const name = item.Name;
@ -87,7 +87,7 @@ function NameLink({ item }: { item: DecoratedStack }) {
); );
} }
if (!isAdmin && isOrphanedStack(item)) { if (!isAdminQuery.isAdmin && isOrphanedStack(item)) {
return <>{name}</>; return <>{name}</>;
} }

View File

@ -4,6 +4,7 @@ import { notifySuccess } from '@/portainer/services/notifications';
import { useDeleteEnvironmentsMutation } from '@/react/portainer/environments/queries/useDeleteEnvironmentsMutation'; import { useDeleteEnvironmentsMutation } from '@/react/portainer/environments/queries/useDeleteEnvironmentsMutation';
import { Environment } from '@/react/portainer/environments/types'; import { Environment } from '@/react/portainer/environments/types';
import { withReactQuery } from '@/react-tools/withReactQuery'; import { withReactQuery } from '@/react-tools/withReactQuery';
import { useIsPureAdmin } from '@/react/hooks/useUser';
import { Button } from '@@/buttons'; import { Button } from '@@/buttons';
import { ModalType, openModal } from '@@/modals'; import { ModalType, openModal } from '@@/modals';
@ -28,6 +29,7 @@ export function TableActions({
}: { }: {
selectedRows: WaitingRoomEnvironment[]; selectedRows: WaitingRoomEnvironment[];
}) { }) {
const isPureAdmin = useIsPureAdmin();
const associateMutation = useAssociateDeviceMutation(); const associateMutation = useAssociateDeviceMutation();
const removeMutation = useDeleteEnvironmentsMutation(); const removeMutation = useDeleteEnvironmentsMutation();
const licenseOverused = useLicenseOverused(selectedRows.length); const licenseOverused = useLicenseOverused(selectedRows.length);
@ -58,7 +60,9 @@ export function TableActions({
<span> <span>
<Button <Button
onClick={() => handleAssociateAndAssign(selectedRows)} onClick={() => handleAssociateAndAssign(selectedRows)}
disabled={selectedRows.length === 0 || licenseOverused} disabled={
selectedRows.length === 0 || licenseOverused || !isPureAdmin
}
color="secondary" color="secondary"
icon={CheckCircle} icon={CheckCircle}
> >

View File

@ -1,13 +1,10 @@
import { useRouter } from '@uirouter/react'; import { useRouter } from '@uirouter/react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { EnvironmentId } from '../portainer/environments/types';
import { useAuthorizations } from './useUser'; import { useAuthorizations } from './useUser';
type AuthorizationOptions = { type AuthorizationOptions = {
authorizations: string | string[]; authorizations: string | string[];
forceEnvironmentId?: EnvironmentId;
adminOnlyCE?: boolean; adminOnlyCE?: boolean;
}; };
@ -19,24 +16,19 @@ type RedirectOptions = {
/** /**
* Redirects to the given route if the user is not authorized. * Redirects to the given route if the user is not authorized.
* @param authorizations The authorizations to check. * @param authorizations The authorizations to check.
* @param forceEnvironmentId The environment id to use for the check. * @param adminOnlyCE Whether to allow non-admin users in CE.
* @param adminOnlyCE Whether to check only for admin authorizations in CE.
* @param to The route to redirect to. * @param to The route to redirect to.
* @param params The params to pass to the route. * @param params The params to pass to the route.
*/ */
export function useUnauthorizedRedirect( export function useUnauthorizedRedirect(
{ { authorizations, adminOnlyCE = false }: AuthorizationOptions,
authorizations,
forceEnvironmentId,
adminOnlyCE = false,
}: AuthorizationOptions,
{ to, params }: RedirectOptions { to, params }: RedirectOptions
) { ) {
const router = useRouter(); const router = useRouter();
const isAuthorized = useAuthorizations( const isAuthorized = useAuthorizations(
authorizations, authorizations,
forceEnvironmentId, undefined,
adminOnlyCE adminOnlyCE
); );

View File

@ -7,11 +7,14 @@ import {
PropsWithChildren, PropsWithChildren,
} from 'react'; } from 'react';
import { isAdmin } from '@/portainer/users/user.helpers'; import { isEdgeAdmin, isPureAdmin } from '@/portainer/users/user.helpers';
import { EnvironmentId } from '@/react/portainer/environments/types'; import { EnvironmentId } from '@/react/portainer/environments/types';
import { User } from '@/portainer/users/types'; import { User } from '@/portainer/users/types';
import { useLoadCurrentUser } from '@/portainer/users/queries/useLoadCurrentUser'; import { useLoadCurrentUser } from '@/portainer/users/queries/useLoadCurrentUser';
import { useEnvironment } from '../portainer/environments/queries';
import { isBE } from '../portainer/feature-flags/feature-flags.service';
interface State { interface State {
user?: User; user?: User;
} }
@ -39,32 +42,84 @@ export function useCurrentUser() {
return useMemo( return useMemo(
() => ({ () => ({
user, user,
isAdmin: isAdmin(user), isPureAdmin: isPureAdmin(user),
}), }),
[user] [user]
); );
} }
export function useIsPureAdmin() {
const { isPureAdmin } = useCurrentUser();
return isPureAdmin;
}
/**
* Load the admin status of the user, (admin >= edge admin)
* @param forceEnvironmentId to force the environment id, used where the environment id can't be loaded from the router, like sidebar
* @returns query result with isLoading and isAdmin - isAdmin is true if the user edge admin or admin.
*/
export function useIsEdgeAdmin({
forceEnvironmentId,
noEnvScope,
}: {
forceEnvironmentId?: EnvironmentId;
noEnvScope?: boolean;
} = {}) {
const { user } = useCurrentUser();
const {
params: { endpointId },
} = useCurrentStateAndParams();
const envId = forceEnvironmentId || endpointId;
const envScope = typeof noEnvScope === 'boolean' ? !noEnvScope : !!envId;
const envQuery = useEnvironment(envScope ? envId : undefined);
if (!envScope) {
return { isLoading: false, isAdmin: isEdgeAdmin(user) };
}
if (envQuery.isLoading) {
return { isLoading: true, isAdmin: false };
}
return {
isLoading: false,
isAdmin: isEdgeAdmin(user, envQuery.data),
};
}
export function useAuthorizations( export function useAuthorizations(
authorizations: string | string[], authorizations: string | string[],
forceEnvironmentId?: EnvironmentId, forceEnvironmentId?: EnvironmentId,
adminOnlyCE = false adminOnlyCE = false
) { ) {
const { user } = useUser(); const { user } = useCurrentUser();
const { const {
params: { endpointId }, params: { endpointId },
} = useCurrentStateAndParams(); } = useCurrentStateAndParams();
const envQuery = useEnvironment(forceEnvironmentId || endpointId);
const isAdmin = useIsEdgeAdmin({ forceEnvironmentId });
if (!user) { if (!user) {
return false; return { authorized: false, isLoading: false };
} }
return hasAuthorizations( if (envQuery.isLoading) {
user, return { authorized: false, isLoading: true };
authorizations, }
forceEnvironmentId || endpointId,
adminOnlyCE if (isAdmin) {
); return { authorized: true, isLoading: false };
}
if (!isBE && adminOnlyCE) {
return { authorized: false, isLoading: false };
}
return {
authorized: hasAuthorizations(user, authorizations, envQuery.data?.Id),
isLoading: false,
};
} }
export function useIsEnvironmentAdmin({ export function useIsEnvironmentAdmin({
@ -81,24 +136,18 @@ export function useIsEnvironmentAdmin({
); );
} }
export function isEnvironmentAdmin( /**
user: User, * will return true if the user has the authorizations. assumes the user is authenticated and not an admin
environmentId: EnvironmentId, * @param user
adminOnlyCE = true * @param authorizations
) { * @param environmentId
return hasAuthorizations( * @param adminOnlyCE
user, * @returns
['EndpointResourcesAccess'], */
environmentId, function hasAuthorizations(
adminOnlyCE
);
}
export function hasAuthorizations(
user: User, user: User,
authorizations: string | string[], authorizations: string | string[],
environmentId?: EnvironmentId, environmentId?: EnvironmentId
adminOnlyCE = false
) { ) {
const authorizationsArray = const authorizationsArray =
typeof authorizations === 'string' ? [authorizations] : authorizations; typeof authorizations === 'string' ? [authorizations] : authorizations;
@ -107,26 +156,13 @@ export function hasAuthorizations(
return true; return true;
} }
if (process.env.PORTAINER_EDITION === 'CE') {
return !adminOnlyCE || isAdmin(user);
}
if (!environmentId) { if (!environmentId) {
return false; return false;
} }
if (isAdmin(user)) { const userEndpointAuthorizations =
return true; user.EndpointAuthorizations?.[environmentId] || [];
}
if (
!user.EndpointAuthorizations ||
!user.EndpointAuthorizations[environmentId]
) {
return false;
}
const userEndpointAuthorizations = user.EndpointAuthorizations[environmentId];
return authorizationsArray.some( return authorizationsArray.some(
(authorization) => userEndpointAuthorizations[authorization] (authorization) => userEndpointAuthorizations[authorization]
); );

View File

@ -1,6 +1,6 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useCurrentUser } from '@/react/hooks/useUser'; import { useIsEdgeAdmin } from '@/react/hooks/useUser';
import { Link } from '@@/Link'; import { Link } from '@@/Link';
import { TextTip } from '@@/Tip/TextTip'; import { TextTip } from '@@/Tip/TextTip';
@ -22,11 +22,14 @@ export function StackName({
inputClassName, inputClassName,
textTip = "Enter or select a 'stack' name to group multiple deployments together, or else leave empty to ignore.", textTip = "Enter or select a 'stack' name to group multiple deployments together, or else leave empty to ignore.",
}: Props) { }: Props) {
const { isAdmin } = useCurrentUser(); const isAdminQuery = useIsEdgeAdmin();
const stackResults = useMemo( const stackResults = useMemo(
() => stacks.filter((stack) => stack.includes(stackName ?? '')), () => stacks.filter((stack) => stack.includes(stackName ?? '')),
[stacks, stackName] [stacks, stackName]
); );
const { isAdmin } = isAdminQuery;
const tooltip = ( const tooltip = (
<> <>
You may specify a stack name to label resources that you want to group. You may specify a stack name to label resources that you want to group.

View File

@ -1,7 +1,7 @@
import { Plus, RefreshCw } from 'lucide-react'; import { Plus, RefreshCw } from 'lucide-react';
import { FormikErrors } from 'formik'; import { FormikErrors } from 'formik';
import { useCurrentUser } from '@/react/hooks/useUser'; import { useIsEdgeAdmin } from '@/react/hooks/useUser';
import { useEnvironment } from '@/react/portainer/environments/queries'; import { useEnvironment } from '@/react/portainer/environments/queries';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId'; import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
@ -39,7 +39,8 @@ export function LoadBalancerServicesForm({
namespace, namespace,
isEditMode, isEditMode,
}: Props) { }: Props) {
const { isAdmin } = useCurrentUser(); const isAdminQuery = useIsEdgeAdmin();
const environmentId = useEnvironmentId(); const environmentId = useEnvironmentId();
const { data: loadBalancerEnabled, ...loadBalancerEnabledQuery } = const { data: loadBalancerEnabled, ...loadBalancerEnabledQuery } =
useEnvironment( useEnvironment(
@ -47,6 +48,12 @@ export function LoadBalancerServicesForm({
(environment) => environment?.Kubernetes.Configuration.UseLoadBalancer (environment) => environment?.Kubernetes.Configuration.UseLoadBalancer
); );
if (isAdminQuery.isLoading) {
return null;
}
const { isAdmin } = isAdminQuery;
const loadBalancerServiceCount = services.filter( const loadBalancerServiceCount = services.filter(
(service) => service.Type === 'LoadBalancer' (service) => service.Type === 'LoadBalancer'
).length; ).length;

View File

@ -1,6 +1,6 @@
import { FormikErrors } from 'formik'; import { FormikErrors } from 'formik';
import { useCurrentUser } from '@/react/hooks/useUser'; import { useIsEdgeAdmin } from '@/react/hooks/useUser';
import { SwitchField } from '@@/form-components/SwitchField'; import { SwitchField } from '@@/form-components/SwitchField';
import { Link } from '@@/Link'; import { Link } from '@@/Link';
@ -113,7 +113,13 @@ export function AutoScalingFormSection({
} }
function NoMetricsServerWarning() { function NoMetricsServerWarning() {
const { isAdmin } = useCurrentUser(); const isAdminQuery = useIsEdgeAdmin();
if (isAdminQuery.isLoading) {
return null;
}
const { isAdmin } = isAdminQuery;
return ( return (
<TextTip color="orange"> <TextTip color="orange">
{isAdmin && ( {isAdmin && (

View File

@ -12,7 +12,6 @@ export function ConfigureView() {
useUnauthorizedRedirect( useUnauthorizedRedirect(
{ {
authorizations: 'K8sClusterW', authorizations: 'K8sClusterW',
forceEnvironmentId: environment?.Id,
adminOnlyCE: false, adminOnlyCE: false,
}, },
{ {

View File

@ -12,7 +12,6 @@ export function CreateNamespaceView() {
useUnauthorizedRedirect( useUnauthorizedRedirect(
{ {
authorizations: 'K8sResourcePoolsW', authorizations: 'K8sResourcePoolsW',
forceEnvironmentId: environmentId,
adminOnlyCE: !isBE, adminOnlyCE: !isBE,
}, },
{ {

View File

@ -19,13 +19,13 @@ export function RegistriesSelector({
options = [], options = [],
inputId, inputId,
}: Props) { }: Props) {
const { isAdmin } = useCurrentUser(); const { isPureAdmin } = useCurrentUser();
return ( return (
<> <>
{options.length === 0 && ( {options.length === 0 && (
<p className="text-muted text-xs mb-1 mt-2"> <p className="text-muted text-xs mb-1 mt-2">
{isAdmin ? ( {isPureAdmin ? (
<span> <span>
No registries available. Head over to the{' '} No registries available. Head over to the{' '}
<Link to="portainer.registries" target="_blank"> <Link to="portainer.registries" target="_blank">

View File

@ -2,7 +2,7 @@ import { Edit2, Settings } from 'lucide-react';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { useUser } from '@/react/hooks/useUser'; import { useCurrentUser } from '@/react/hooks/useUser';
import { import {
Environment, Environment,
PlatformType, PlatformType,
@ -15,7 +15,7 @@ import {
import { LinkButton } from '@@/LinkButton'; import { LinkButton } from '@@/LinkButton';
export function EditButtons({ environment }: { environment: Environment }) { export function EditButtons({ environment }: { environment: Environment }) {
const { isAdmin } = useUser(); const { isPureAdmin } = useCurrentUser();
const isEdgeAsync = checkEdgeAsync(environment); const isEdgeAsync = checkEdgeAsync(environment);
@ -31,7 +31,7 @@ export function EditButtons({ environment }: { environment: Environment }) {
return ( return (
<ButtonsGrid className="ml-3 w-11"> <ButtonsGrid className="ml-3 w-11">
<LinkButton <LinkButton
disabled={!isAdmin} disabled={!isPureAdmin}
to="portainer.endpoints.endpoint" to="portainer.endpoints.endpoint"
params={{ id: environment.Id, redirectTo: 'portainer.home' }} params={{ id: environment.Id, redirectTo: 'portainer.home' }}
color="none" color="none"
@ -42,7 +42,7 @@ export function EditButtons({ environment }: { environment: Environment }) {
/> />
<LinkButton <LinkButton
disabled={!configRoute || isEdgeAsync || !isAdmin} disabled={!configRoute || isEdgeAsync || !isPureAdmin}
to={configRoute} to={configRoute}
params={{ endpointId: environment.Id }} params={{ endpointId: environment.Id }}
color="none" color="none"

View File

@ -18,7 +18,7 @@ import {
} from '@/react/portainer/environments/queries/useEnvironmentList'; } from '@/react/portainer/environments/queries/useEnvironmentList';
import { useGroups } from '@/react/portainer/environments/environment-groups/queries'; import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
import { EnvironmentsQueryParams } from '@/react/portainer/environments/environment.service'; import { EnvironmentsQueryParams } from '@/react/portainer/environments/environment.service';
import { useUser } from '@/react/hooks/useUser'; import { useIsPureAdmin } from '@/react/hooks/useUser';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { environmentStore } from '@/react/hooks/current-environment-store'; import { environmentStore } from '@/react/hooks/current-environment-store';
@ -46,7 +46,7 @@ interface Props {
const storageKey = 'home_endpoints'; const storageKey = 'home_endpoints';
export function EnvironmentList({ onClickBrowse, onRefresh }: Props) { export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
const { isAdmin } = useUser(); const isPureAdmin = useIsPureAdmin();
const currentEnvStore = useStore(environmentStore); const currentEnvStore = useStore(environmentStore);
const [platformTypes, setPlatformTypes] = useHomePageFilter<PlatformType[]>( const [platformTypes, setPlatformTypes] = useHomePageFilter<PlatformType[]>(
@ -138,7 +138,9 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
return ( return (
<> <>
{totalAvailable === 0 && <NoEnvironmentsInfoPanel isAdmin={isAdmin} />} {totalAvailable === 0 && (
<NoEnvironmentsInfoPanel isAdmin={isPureAdmin} />
)}
<TableContainer> <TableContainer>
<div className="px-4"> <div className="px-4">
@ -160,7 +162,7 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
placeholder="Search by name, group, tag, status, URL..." placeholder="Search by name, group, tag, status, URL..."
data-cy="home-endpointsSearchInput" data-cy="home-endpointsSearchInput"
/> />
{isAdmin && ( {isPureAdmin && (
<Button <Button
onClick={onRefresh} onClick={onRefresh}
data-cy="home-refreshEndpointsButton" data-cy="home-refreshEndpointsButton"

View File

@ -1,6 +1,6 @@
import { FormikErrors } from 'formik'; import { FormikErrors } from 'formik';
import { useUser } from '@/react/hooks/useUser'; import { useIsEdgeAdmin } from '@/react/hooks/useUser';
import { FormSectionTitle } from '@@/form-components/FormSectionTitle'; import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
import { SwitchField } from '@@/form-components/SwitchField'; import { SwitchField } from '@@/form-components/SwitchField';
@ -26,7 +26,13 @@ export function AccessControlForm({
errors, errors,
environmentId, environmentId,
}: Props) { }: Props) {
const { isAdmin } = useUser(); const isAdminQuery = useIsEdgeAdmin();
if (isAdminQuery.isLoading) {
return null;
}
const { isAdmin } = isAdminQuery;
const accessControlEnabled = const accessControlEnabled =
values.ownership !== ResourceControlOwnership.PUBLIC; values.ownership !== ResourceControlOwnership.PUBLIC;

View File

@ -1,11 +1,14 @@
import { useReducer } from 'react'; import { useReducer } from 'react';
import { Edit, Eye } from 'lucide-react'; import { Edit, Eye } from 'lucide-react';
import { useUser } from '@/react/hooks/useUser';
import { Icon } from '@/react/components/Icon'; import { Icon } from '@/react/components/Icon';
import { TeamMembership, TeamRole } from '@/react/portainer/users/teams/types'; import { TeamMembership, TeamRole } from '@/react/portainer/users/teams/types';
import { useIsTeamLeader, useUserMembership } from '@/portainer/users/queries'; import {
useIsCurrentUserTeamLeader,
useUserMembership,
} from '@/portainer/users/queries';
import { EnvironmentId } from '@/react/portainer/environments/types'; import { EnvironmentId } from '@/react/portainer/environments/types';
import { useCurrentUser, useIsEdgeAdmin } from '@/react/hooks/useUser';
import { TableContainer, TableTitle } from '@@/datatables'; import { TableContainer, TableTitle } from '@@/datatables';
import { Button } from '@@/buttons'; import { Button } from '@@/buttons';
@ -34,20 +37,27 @@ export function AccessControlPanel({
onUpdateSuccess, onUpdateSuccess,
}: Props) { }: Props) {
const [isEditMode, toggleEditMode] = useReducer((state) => !state, false); const [isEditMode, toggleEditMode] = useReducer((state) => !state, false);
const { user, isAdmin } = useUser(); const isAdminQuery = useIsEdgeAdmin();
const isTeamLeader = useIsCurrentUserTeamLeader();
const isInherited = checkIfInherited(); const isInherited = checkIfInherited();
const restrictions = useRestrictions(resourceControl);
if (isAdminQuery.isLoading || !restrictions) {
return null;
}
const { isPartOfRestrictedUsers, isLeaderOfAnyRestrictedTeams } = const { isPartOfRestrictedUsers, isLeaderOfAnyRestrictedTeams } =
useRestrictions(resourceControl); restrictions;
const { isAdmin } = isAdminQuery;
const isEditDisabled = const isEditDisabled =
disableOwnershipChange || disableOwnershipChange ||
isInherited || isInherited ||
(!isAdmin && !isPartOfRestrictedUsers && !isLeaderOfAnyRestrictedTeams); (!isAdmin && !isPartOfRestrictedUsers && !isLeaderOfAnyRestrictedTeams);
const isTeamLeader = useIsTeamLeader(user) as boolean;
return ( return (
<TableContainer> <TableContainer>
<TableTitle label="Access control" icon={Eye} /> <TableTitle label="Access control" icon={Eye} />
@ -106,10 +116,16 @@ export function AccessControlPanel({
} }
function useRestrictions(resourceControl?: ResourceControlViewModel) { function useRestrictions(resourceControl?: ResourceControlViewModel) {
const { user, isAdmin } = useUser(); const { user } = useCurrentUser();
const isAdminQuery = useIsEdgeAdmin();
const memberships = useUserMembership(user.Id); const memberships = useUserMembership(user.Id);
if (isAdminQuery.isLoading) {
return undefined;
}
const { isAdmin } = isAdminQuery;
if (!resourceControl || isAdmin) { if (!resourceControl || isAdmin) {
return { return {
isPartOfRestrictedUsers: false, isPartOfRestrictedUsers: false,

View File

@ -3,7 +3,7 @@ import clsx from 'clsx';
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
import { object } from 'yup'; import { object } from 'yup';
import { useCurrentUser } from '@/react/hooks/useUser'; import { useCurrentUser, useIsEdgeAdmin } from '@/react/hooks/useUser';
import { notifySuccess } from '@/portainer/services/notifications'; import { notifySuccess } from '@/portainer/services/notifications';
import { EnvironmentId } from '@/react/portainer/environments/types'; import { EnvironmentId } from '@/react/portainer/environments/types';
@ -43,7 +43,8 @@ export function AccessControlPanelForm({
onCancelClick, onCancelClick,
onUpdateSuccess, onUpdateSuccess,
}: Props) { }: Props) {
const { isAdmin, user } = useCurrentUser(); const { user } = useCurrentUser();
const isAdminQuery = useIsEdgeAdmin();
const updateAccess = useMutation( const updateAccess = useMutation(
(variables: AccessControlFormData) => (variables: AccessControlFormData) =>
@ -63,6 +64,12 @@ export function AccessControlPanelForm({
} }
); );
if (isAdminQuery.isLoading) {
return null;
}
const { isAdmin } = isAdminQuery;
const initialValues = { const initialValues = {
accessControl: parseAccessControlFormData( accessControl: parseAccessControlFormData(
isAdmin, isAdmin,

View File

@ -1,7 +1,7 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { FormikErrors } from 'formik'; import { FormikErrors } from 'formik';
import { useUser } from '@/react/hooks/useUser'; import { useCurrentUser } from '@/react/hooks/useUser';
import { EnvironmentId } from '@/react/portainer/environments/types'; import { EnvironmentId } from '@/react/portainer/environments/types';
import { FormError } from '@@/form-components/FormError'; import { FormError } from '@@/form-components/FormError';
@ -30,9 +30,10 @@ export function EditDetails({
formNamespace, formNamespace,
environmentId, environmentId,
}: Props) { }: Props) {
const { user, isAdmin } = useUser(); const { user, isPureAdmin } = useCurrentUser();
const { users, teams, isLoading } = useLoadState(environmentId);
const { users, teams, isLoading } = useLoadState(environmentId, isAdmin);
const handleChange = useCallback( const handleChange = useCallback(
(partialValues: Partial<typeof values>) => { (partialValues: Partial<typeof values>) => {
onChange({ ...values, ...partialValues }); onChange({ ...values, ...partialValues });
@ -41,7 +42,12 @@ export function EditDetails({
[values, onChange] [values, onChange]
); );
if (isLoading || !teams || (isAdmin && !users) || !values.authorizedUsers) { if (
isLoading ||
!teams ||
(isPureAdmin && !users) ||
!values.authorizedUsers
) {
return null; return null;
} }
@ -51,14 +57,14 @@ export function EditDetails({
onChange={handleChangeOwnership} onChange={handleChangeOwnership}
name={withNamespace('ownership')} name={withNamespace('ownership')}
value={values.ownership} value={values.ownership}
isAdmin={isAdmin} isAdmin={isPureAdmin}
isPublicVisible={isPublicVisible} isPublicVisible={isPublicVisible}
teams={teams} teams={teams}
/> />
{values.ownership === ResourceControlOwnership.RESTRICTED && ( {values.ownership === ResourceControlOwnership.RESTRICTED && (
<div aria-label="extra-options"> <div aria-label="extra-options">
{isAdmin && ( {isPureAdmin && (
<UsersField <UsersField
name={withNamespace('authorizedUsers')} name={withNamespace('authorizedUsers')}
users={users || []} users={users || []}
@ -68,12 +74,12 @@ export function EditDetails({
/> />
)} )}
{(isAdmin || teams.length > 1) && ( {(isPureAdmin || teams.length > 1) && (
<TeamsField <TeamsField
name={withNamespace('authorizedTeams')} name={withNamespace('authorizedTeams')}
teams={teams} teams={teams}
overrideTooltip={ overrideTooltip={
!isAdmin && teams.length > 1 !isPureAdmin && teams.length > 1
? 'As you are a member of multiple teams, you can select which teams(s) will be able to manage this resource.' ? 'As you are a member of multiple teams, you can select which teams(s) will be able to manage this resource.'
: undefined : undefined
} }
@ -111,7 +117,7 @@ export function EditDetails({
// Non admin team leaders/members under only one team can // Non admin team leaders/members under only one team can
// automatically grant the resource access to all members // automatically grant the resource access to all members
// under the team // under the team
if (!isAdmin && teams && teams.length === 1) { if (!isPureAdmin && teams && teams.length === 1) {
authorizedTeams = teams.map((team) => team.Id); authorizedTeams = teams.map((team) => team.Id);
} }
} }

View File

@ -1,15 +1,18 @@
import { useTeams } from '@/react/portainer/users/teams/queries'; import { useTeams } from '@/react/portainer/users/teams/queries';
import { useUsers } from '@/portainer/users/queries'; import { useUsers } from '@/portainer/users/queries';
import { EnvironmentId } from '@/react/portainer/environments/types'; import { EnvironmentId } from '@/react/portainer/environments/types';
import { useIsEdgeAdmin } from '@/react/hooks/useUser';
export function useLoadState(environmentId: EnvironmentId, enabled = true) { export function useLoadState(environmentId: EnvironmentId) {
const isAdminQuery = useIsEdgeAdmin();
const teams = useTeams(false, environmentId); const teams = useTeams(false, environmentId);
const users = useUsers(false, environmentId, enabled); const users = useUsers(false, environmentId, isAdminQuery.isAdmin);
return { return {
teams: teams.data, teams: teams.data,
users: users.data, users: users.data,
isLoading: teams.isLoading || users.isLoading, isAdmin: isAdminQuery.isAdmin,
isLoading: teams.isLoading || users.isLoading || isAdminQuery.isLoading,
}; };
} }

View File

@ -7,14 +7,14 @@ import { Environment, EnvironmentId } from '../types';
import { environmentQueryKeys } from './query-keys'; import { environmentQueryKeys } from './query-keys';
export function useEnvironment<T = Environment | null>( export function useEnvironment<T = Environment>(
environmentId?: EnvironmentId, environmentId?: EnvironmentId,
select?: (environment: Environment | null) => T, select?: (environment: Environment) => T,
options?: { autoRefreshRate?: number } options?: { autoRefreshRate?: number }
) { ) {
return useQuery( return useQuery(
environmentId ? environmentQueryKeys.item(environmentId) : [], environmentQueryKeys.item(environmentId!),
() => (environmentId ? getEndpoint(environmentId) : null), () => getEndpoint(environmentId!),
{ {
select, select,
...withError('Failed loading environment'), ...withError('Failed loading environment'),

View File

@ -1,24 +1,27 @@
import { useField } from 'formik'; import { useField } from 'formik';
import { PropsWithChildren } from 'react';
import { useUser } from '@/react/hooks/useUser'; import { useCurrentUser } from '@/react/hooks/useUser';
import { TagSelector } from '@@/TagSelector'; import { TagSelector } from '@@/TagSelector';
import { FormSection } from '@@/form-components/FormSection'; import { FormSection } from '@@/form-components/FormSection';
import { GroupField } from './GroupsField'; import { GroupField } from './GroupsField';
export function MetadataFieldset() { export function MetadataFieldset({ children }: PropsWithChildren<unknown>) {
const [tagProps, , tagHelpers] = useField('meta.tagIds'); const [tagProps, , tagHelpers] = useField('meta.tagIds');
const { isAdmin } = useUser(); const { isPureAdmin } = useCurrentUser();
return ( return (
<FormSection title="Metadata"> <FormSection title="Metadata">
{children}
<GroupField /> <GroupField />
<TagSelector <TagSelector
value={tagProps.value} value={tagProps.value}
allowCreate={isAdmin} allowCreate={isPureAdmin}
onChange={(value) => tagHelpers.setValue(value)} onChange={(value) => tagHelpers.setValue(value)}
/> />
</FormSection> </FormSection>

View File

@ -1,7 +1,7 @@
import { useCurrentStateAndParams } from '@uirouter/react'; import { useCurrentStateAndParams } from '@uirouter/react';
import { parseAccessControlFormData } from '@/react/portainer/access-control/utils'; import { parseAccessControlFormData } from '@/react/portainer/access-control/utils';
import { useCurrentUser } from '@/react/hooks/useUser'; import { useCurrentUser, useIsEdgeAdmin } from '@/react/hooks/useUser';
import { StackType } from '@/react/common/stacks/types'; import { StackType } from '@/react/common/stacks/types';
import { Platform } from '../../types'; import { Platform } from '../../types';
@ -19,11 +19,13 @@ export function useInitialValues({
isEdge?: boolean; isEdge?: boolean;
buildMethods: Array<Method>; buildMethods: Array<Method>;
}): FormValues | undefined { }): FormValues | undefined {
const { user, isAdmin } = useCurrentUser(); const { user } = useCurrentUser();
const isAdminQuery = useIsEdgeAdmin();
const { appTemplateId, type = defaultType } = useAppTemplateParams(); const { appTemplateId, type = defaultType } = useAppTemplateParams();
const fileContentQuery = useFetchTemplateFile(appTemplateId); const fileContentQuery = useFetchTemplateFile(appTemplateId);
if (fileContentQuery.isLoading) { if (fileContentQuery.isLoading || isAdminQuery.isLoading) {
return undefined; return undefined;
} }
@ -51,7 +53,7 @@ export function useInitialValues({
}, },
AccessControl: isEdge AccessControl: isEdge
? undefined ? undefined
: parseAccessControlFormData(isAdmin, user.Id), : parseAccessControlFormData(isAdminQuery.isAdmin, user.Id),
EdgeSettings: isEdge ? getDefaultEdgeTemplateSettings() : undefined, EdgeSettings: isEdge ? getDefaultEdgeTemplateSettings() : undefined,
}; };
} }

View File

@ -46,7 +46,7 @@ export function EditForm({
templateFile: fileContentQuery.data, templateFile: fileContentQuery.data,
}); });
if (fileContentQuery.isLoading) { if (fileContentQuery.isLoading || !initialValues) {
return null; return null;
} }

View File

@ -1,5 +1,5 @@
import { parseAccessControlFormData } from '@/react/portainer/access-control/utils'; import { parseAccessControlFormData } from '@/react/portainer/access-control/utils';
import { useCurrentUser } from '@/react/hooks/useUser'; import { useCurrentUser, useIsEdgeAdmin } from '@/react/hooks/useUser';
import { toGitFormModel } from '@/react/portainer/gitops/types'; import { toGitFormModel } from '@/react/portainer/gitops/types';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel'; import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
@ -15,8 +15,14 @@ export function useInitialValues({
template: CustomTemplate; template: CustomTemplate;
templateFile: string | undefined; templateFile: string | undefined;
isEdge: boolean; isEdge: boolean;
}): FormValues { }): FormValues | undefined {
const { user, isAdmin } = useCurrentUser(); const { user } = useCurrentUser();
const isAdminQuery = useIsEdgeAdmin();
if (isAdminQuery.isLoading) {
return undefined;
}
return { return {
Title: template.Title, Title: template.Title,
@ -31,7 +37,7 @@ export function useInitialValues({
AccessControl: AccessControl:
!isEdge && template.ResourceControl !isEdge && template.ResourceControl
? parseAccessControlFormData( ? parseAccessControlFormData(
isAdmin, isAdminQuery.isAdmin,
user.Id, user.Id,
new ResourceControlViewModel(template.ResourceControl) new ResourceControlViewModel(template.ResourceControl)
) )

View File

@ -1,6 +1,6 @@
import { Edit, Trash2 } from 'lucide-react'; import { Edit, Trash2 } from 'lucide-react';
import { useCurrentUser } from '@/react/hooks/useUser'; import { useCurrentUser, useIsEdgeAdmin } from '@/react/hooks/useUser';
import { StackType } from '@/react/common/stacks/types'; import { StackType } from '@/react/common/stacks/types';
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types'; import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
@ -22,8 +22,15 @@ export function CustomTemplatesListItem({
isSelected: boolean; isSelected: boolean;
linkParams?: { to: string; params: object }; linkParams?: { to: string; params: object };
}) { }) {
const { isAdmin, user } = useCurrentUser(); const { user } = useCurrentUser();
const isEditAllowed = isAdmin || template.CreatedByUserId === user.Id; const isAdminQuery = useIsEdgeAdmin();
if (isAdminQuery.isLoading) {
return null;
}
const isEditAllowed =
isAdminQuery.isAdmin || template.CreatedByUserId === user.Id;
return ( return (
<TemplateItem <TemplateItem

View File

@ -1,7 +1,7 @@
import { useRouter } from '@uirouter/react'; import { useRouter } from '@uirouter/react';
import { useUsers } from '@/portainer/users/queries'; import { useUsers } from '@/portainer/users/queries';
import { useUser } from '@/react/hooks/useUser'; import { useIsPureAdmin } from '@/react/hooks/useUser';
import { usePublicSettings } from '@/react/portainer/settings/queries'; import { usePublicSettings } from '@/react/portainer/settings/queries';
import { TextTip } from '@@/Tip/TextTip'; import { TextTip } from '@@/Tip/TextTip';
@ -16,7 +16,7 @@ import { useTeamIdParam } from './useTeamIdParam';
export function ItemView() { export function ItemView() {
const teamId = useTeamIdParam(); const teamId = useTeamIdParam();
const { isAdmin } = useUser(); const isPureAdmin = useIsPureAdmin();
const router = useRouter(); const router = useRouter();
const teamQuery = useTeam(teamId, () => const teamQuery = useTeam(teamId, () =>
router.stateService.go('portainer.teams') router.stateService.go('portainer.teams')
@ -45,7 +45,7 @@ export function ItemView() {
<Details <Details
team={team} team={team}
memberships={membershipsQuery.data} memberships={membershipsQuery.data}
isAdmin={isAdmin} isAdmin={isPureAdmin}
/> />
)} )}

View File

@ -3,7 +3,7 @@ import { Users, UserX } from 'lucide-react';
import { User, UserId } from '@/portainer/users/types'; import { User, UserId } from '@/portainer/users/types';
import { TeamId, TeamRole } from '@/react/portainer/users/teams/types'; import { TeamId, TeamRole } from '@/react/portainer/users/teams/types';
import { useUser } from '@/react/hooks/useUser'; import { useIsPureAdmin } from '@/react/hooks/useUser';
import { notifySuccess } from '@/portainer/services/notifications'; import { notifySuccess } from '@/portainer/services/notifications';
import { import {
useRemoveMemberMutation, useRemoveMemberMutation,
@ -37,8 +37,7 @@ export function TeamMembersList({ users, roles, disabled, teamId }: Props) {
{ id: string; desc: boolean } | undefined { id: string; desc: boolean } | undefined
>({ id: 'name', desc: false }); >({ id: 'name', desc: false });
const { isAdmin } = useUser(); const isPureAdmin = useIsPureAdmin();
const rowContext = useMemo<RowContext>( const rowContext = useMemo<RowContext>(
() => ({ () => ({
getRole(userId: UserId) { getRole(userId: UserId) {
@ -58,7 +57,7 @@ export function TeamMembersList({ users, roles, disabled, teamId }: Props) {
titleIcon={Users} titleIcon={Users}
title="Team members" title="Team members"
renderTableActions={() => renderTableActions={() =>
isAdmin && ( isPureAdmin && (
<Button <Button
onClick={() => handleRemoveMembers(users.map((user) => user.Id))} onClick={() => handleRemoveMembers(users.map((user) => user.Id))}
disabled={disabled || users.length === 0} disabled={disabled || users.length === 0}

View File

@ -2,7 +2,7 @@ import { User as UserIcon, UserPlus, UserX } from 'lucide-react';
import { CellContext } from '@tanstack/react-table'; import { CellContext } from '@tanstack/react-table';
import { User } from '@/portainer/users/types'; import { User } from '@/portainer/users/types';
import { useUser as useCurrentUser } from '@/react/hooks/useUser'; import { useCurrentUser } from '@/react/hooks/useUser';
import { TeamRole } from '@/react/portainer/users/teams/types'; import { TeamRole } from '@/react/portainer/users/teams/types';
import { notifySuccess } from '@/portainer/services/notifications'; import { notifySuccess } from '@/portainer/services/notifications';
import { import {
@ -23,7 +23,7 @@ export const teamRole = columnHelper.accessor('Id', {
cell: RoleCell, cell: RoleCell,
}); });
export function RoleCell({ function RoleCell({
row: { original: user }, row: { original: user },
getValue, getValue,
}: CellContext<User, User['Id']>) { }: CellContext<User, User['Id']>) {
@ -38,12 +38,16 @@ export function RoleCell({
const role = getRole(id); const role = getRole(id);
const { isAdmin } = useCurrentUser(); const { isPureAdmin } = useCurrentUser();
const Cell = role === TeamRole.Leader ? LeaderCell : MemberCell; const Cell = role === TeamRole.Leader ? LeaderCell : MemberCell;
return ( return (
<Cell isAdmin={isAdmin} onClick={handleUpdateRole} disabled={disabled} /> <Cell
isAdmin={isPureAdmin}
onClick={handleUpdateRole}
disabled={disabled}
/>
); );
function handleUpdateRole(role: TeamRole, onSuccessMessage: string) { function handleUpdateRole(role: TeamRole, onSuccessMessage: string) {

View File

@ -2,7 +2,7 @@ import { useMemo, useState } from 'react';
import { UserPlus, Users } from 'lucide-react'; import { UserPlus, Users } from 'lucide-react';
import { User, UserId } from '@/portainer/users/types'; import { User, UserId } from '@/portainer/users/types';
import { useUser } from '@/react/hooks/useUser'; import { useCurrentUser } from '@/react/hooks/useUser';
import { notifySuccess } from '@/portainer/services/notifications'; import { notifySuccess } from '@/portainer/services/notifications';
import { useAddMemberMutation } from '@/react/portainer/users/teams/queries'; import { useAddMemberMutation } from '@/react/portainer/users/teams/queries';
import { TeamId } from '@/react/portainer/users/teams/types'; import { TeamId } from '@/react/portainer/users/teams/types';
@ -29,7 +29,7 @@ export function UsersList({ users, disabled, teamId }: Props) {
{ id: string; desc: boolean } | undefined { id: string; desc: boolean } | undefined
>({ id: 'name', desc: false }); >({ id: 'name', desc: false });
const { isAdmin } = useUser(); const { isPureAdmin } = useCurrentUser();
const rowContext = useMemo(() => ({ disabled, teamId }), [disabled, teamId]); const rowContext = useMemo(() => ({ disabled, teamId }), [disabled, teamId]);
@ -41,7 +41,7 @@ export function UsersList({ users, disabled, teamId }: Props) {
titleIcon={Users} titleIcon={Users}
title="Users" title="Users"
renderTableActions={() => renderTableActions={() =>
isAdmin && ( isPureAdmin && (
<Button <Button
onClick={() => handleAddAllMembers(users.map((u) => u.Id))} onClick={() => handleAddAllMembers(users.map((u) => u.Id))}
disabled={disabled || users.length === 0} disabled={disabled || users.length === 0}

View File

@ -1,5 +1,5 @@
import { useUsers } from '@/portainer/users/queries'; import { useUsers } from '@/portainer/users/queries';
import { useUser } from '@/react/hooks/useUser'; import { useCurrentUser } from '@/react/hooks/useUser';
import { PageHeader } from '@@/PageHeader'; import { PageHeader } from '@@/PageHeader';
@ -9,10 +9,10 @@ import { CreateTeamForm } from './CreateTeamForm';
import { TeamsDatatable } from './TeamsDatatable'; import { TeamsDatatable } from './TeamsDatatable';
export function ListView() { export function ListView() {
const { isAdmin } = useUser(); const { isPureAdmin } = useCurrentUser();
const usersQuery = useUsers(false); const usersQuery = useUsers(false);
const teamsQuery = useTeams(!isAdmin, 0, { enabled: !!usersQuery.data }); const teamsQuery = useTeams(!isPureAdmin, 0);
return ( return (
<> <>
@ -22,12 +22,12 @@ export function ListView() {
reload reload
/> />
{isAdmin && usersQuery.data && teamsQuery.data && ( {isPureAdmin && usersQuery.data && teamsQuery.data && (
<CreateTeamForm users={usersQuery.data} teams={teamsQuery.data} /> <CreateTeamForm users={usersQuery.data} teams={teamsQuery.data} />
)} )}
{teamsQuery.data && ( {teamsQuery.data && (
<TeamsDatatable teams={teamsQuery.data} isAdmin={isAdmin} /> <TeamsDatatable teams={teamsQuery.data} isAdmin={isPureAdmin} />
)} )}
</> </>
); );

View File

@ -15,7 +15,7 @@ import {
type Environment, type Environment,
type EnvironmentId, type EnvironmentId,
} from '@/react/portainer/environments/types'; } from '@/react/portainer/environments/types';
import { Authorized, useUser, isEnvironmentAdmin } from '@/react/hooks/useUser'; import { Authorized, useIsEnvironmentAdmin } from '@/react/hooks/useUser';
import { useInfo } from '@/react/docker/proxy/queries/useInfo'; import { useInfo } from '@/react/docker/proxy/queries/useInfo';
import { useApiVersion } from '@/react/docker/proxy/queries/useVersion'; import { useApiVersion } from '@/react/docker/proxy/queries/useVersion';
@ -30,11 +30,11 @@ interface Props {
} }
export function DockerSidebar({ environmentId, environment }: Props) { export function DockerSidebar({ environmentId, environment }: Props) {
const { user } = useUser(); const isEnvironmentAdmin = useIsEnvironmentAdmin({ adminOnlyCE: true });
const isAdmin = isEnvironmentAdmin(user, environmentId);
const areStacksVisible = const areStacksVisible =
isAdmin || environment.SecuritySettings.allowStackManagementForRegularUsers; isEnvironmentAdmin ||
environment.SecuritySettings.allowStackManagementForRegularUsers;
const envInfoQuery = useInfo( const envInfoQuery = useInfo(
environmentId, environmentId,
@ -167,7 +167,7 @@ export function DockerSidebar({ environmentId, environment }: Props) {
/> />
)} )}
{!isSwarmManager && isAdmin && ( {!isSwarmManager && isEnvironmentAdmin && (
<SidebarItem <SidebarItem
to="docker.events" to="docker.events"
params={{ endpointId: environmentId }} params={{ endpointId: environmentId }}

View File

@ -12,7 +12,7 @@ import clsx from 'clsx';
import { useSystemStatus } from '@/react/portainer/system/useSystemStatus'; import { useSystemStatus } from '@/react/portainer/system/useSystemStatus';
import { useSystemVersion } from '@/react/portainer/system/useSystemVersion'; import { useSystemVersion } from '@/react/portainer/system/useSystemVersion';
import { useCurrentUser } from '@/react/hooks/useUser'; import { useIsEdgeAdmin } from '@/react/hooks/useUser';
import { Modal } from '@@/modals'; import { Modal } from '@@/modals';
import { Button } from '@@/buttons'; import { Button } from '@@/buttons';
@ -47,7 +47,7 @@ export function BuildInfoModalButton() {
} }
function BuildInfoModal({ closeModal }: { closeModal: () => void }) { function BuildInfoModal({ closeModal }: { closeModal: () => void }) {
const { isAdmin } = useCurrentUser(); const { isAdmin } = useIsEdgeAdmin({ noEnvScope: true });
const versionQuery = useSystemVersion(); const versionQuery = useSystemVersion();
const statusQuery = useSystemStatus(); const statusQuery = useSystemStatus();

View File

@ -16,17 +16,19 @@ import { SidebarSection } from './SidebarSection';
import { SidebarParent } from './SidebarItem/SidebarParent'; import { SidebarParent } from './SidebarItem/SidebarParent';
interface Props { interface Props {
isPureAdmin: boolean;
isAdmin: boolean; isAdmin: boolean;
isTeamLeader?: boolean; isTeamLeader?: boolean;
} }
export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) { export function SettingsSidebar({ isPureAdmin, isAdmin, isTeamLeader }: Props) {
const teamSyncQuery = usePublicSettings<boolean>({ const teamSyncQuery = usePublicSettings<boolean>({
select: (settings) => settings.TeamSync, select: (settings) => settings.TeamSync,
}); });
const showUsersSection = const isPureAdminOrTeamLeader =
!window.ddExtension && (isAdmin || (isTeamLeader && !teamSyncQuery.data)); isPureAdmin || (isTeamLeader && !teamSyncQuery.data && !isAdmin);
const showUsersSection = !window.ddExtension && isPureAdminOrTeamLeader;
return ( return (
<SidebarSection title="Administration"> <SidebarSection title="Administration">
@ -51,7 +53,7 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
data-cy="portainerSidebar-teams" data-cy="portainerSidebar-teams"
/> />
{isAdmin && ( {isPureAdmin && (
<SidebarItem <SidebarItem
to="portainer.roles" to="portainer.roles"
label="Roles" label="Roles"
@ -61,7 +63,7 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
)} )}
</SidebarParent> </SidebarParent>
)} )}
{isAdmin && ( {isPureAdmin && (
<> <>
<SidebarParent <SidebarParent
label="Environment-related" label="Environment-related"
@ -74,7 +76,7 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
'portainer.tags', 'portainer.tags',
], ],
}} }}
data-cy="k8sSidebar-networking" data-cy="portainerSidebar-environments-area"
> >
<SidebarItem <SidebarItem
label="Environments" label="Environments"
@ -139,13 +141,24 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
</SidebarParent> </SidebarParent>
</> </>
)} )}
{isBE && !isPureAdmin && isAdmin && (
<SidebarParent
label="Environment-related"
icon={HardDrive}
to="portainer.endpoints.updateSchedules"
data-cy="portainerSidebar-environments-area"
>
<EdgeUpdatesSidebarItem />
</SidebarParent>
)}
<SidebarItem <SidebarItem
to="portainer.notifications" to="portainer.notifications"
icon={Bell} icon={Bell}
label="Notifications" label="Notifications"
data-cy="portainerSidebar-notifications" data-cy="portainerSidebar-notifications"
/> />
{isAdmin && ( {isPureAdmin && (
<SidebarParent <SidebarParent
to="portainer.settings" to="portainer.settings"
label="Settings" label="Settings"
@ -158,6 +171,7 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
isSubMenu isSubMenu
ignorePaths={[ ignorePaths={[
'portainer.settings.authentication', 'portainer.settings.authentication',
'portainer.settings.sharedcredentials',
'portainer.settings.edgeCompute', 'portainer.settings.edgeCompute',
]} ]}
data-cy="portainerSidebar-generalSettings" data-cy="portainerSidebar-generalSettings"
@ -178,6 +192,7 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
data-cy="portainerSidebar-cloud" data-cy="portainerSidebar-cloud"
/> />
)} )}
<SidebarItem <SidebarItem
to="portainer.settings.edgeCompute" to="portainer.settings.edgeCompute"
label="Edge Compute" label="Edge Compute"

View File

@ -1,8 +1,8 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { Home } from 'lucide-react'; import { Home } from 'lucide-react';
import { useUser } from '@/react/hooks/useUser'; import { useIsEdgeAdmin, useIsPureAdmin } from '@/react/hooks/useUser';
import { useIsTeamLeader } from '@/portainer/users/queries'; import { useIsCurrentUserTeamLeader } from '@/portainer/users/queries';
import { usePublicSettings } from '@/react/portainer/settings/queries'; import { usePublicSettings } from '@/react/portainer/settings/queries';
import styles from './Sidebar.module.css'; import styles from './Sidebar.module.css';
@ -25,16 +25,19 @@ export function Sidebar() {
} }
function InnerSidebar() { function InnerSidebar() {
const { isAdmin, user } = useUser(); const isPureAdmin = useIsPureAdmin();
const isTeamLeader = useIsTeamLeader(user) as boolean; const isAdminQuery = useIsEdgeAdmin({ noEnvScope: true });
const isTeamLeader = useIsCurrentUserTeamLeader();
const { isOpen } = useSidebarState(); const { isOpen } = useSidebarState();
const settingsQuery = usePublicSettings(); const settingsQuery = usePublicSettings();
if (!settingsQuery.data) { if (!settingsQuery.data || isAdminQuery.isLoading) {
return null; return null;
} }
const { isAdmin } = isAdminQuery;
const { LogoURL } = settingsQuery.data; const { LogoURL } = settingsQuery.data;
return ( return (
@ -66,7 +69,11 @@ function InnerSidebar() {
/> />
<EnvironmentSidebar /> <EnvironmentSidebar />
{isAdmin && <EdgeComputeSidebar />} {isAdmin && <EdgeComputeSidebar />}
<SettingsSidebar isAdmin={isAdmin} isTeamLeader={isTeamLeader} /> <SettingsSidebar
isPureAdmin={isPureAdmin}
isAdmin={isAdmin}
isTeamLeader={isTeamLeader}
/>
</ul> </ul>
</div> </div>
<div className="mt-auto pt-8"> <div className="mt-auto pt-8">

View File

@ -29,7 +29,7 @@ const enabledPlatforms: Array<ContainerPlatform> = [
function UpgradeBEBanner() { function UpgradeBEBanner() {
const { const {
isAdmin, isPureAdmin,
user: { Id }, user: { Id },
} = useCurrentUser(); } = useCurrentUser();
@ -90,7 +90,7 @@ function UpgradeBEBanner() {
function handleClick() { function handleClick() {
trackEvent( trackEvent(
isAdmin ? 'portainer-upgrade-admin' : 'portainer-upgrade-non-admin', isPureAdmin ? 'portainer-upgrade-admin' : 'portainer-upgrade-non-admin',
{ {
category: 'portainer', category: 'portainer',
metadata, metadata,

View File

@ -1,6 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { useUser } from '@/react/hooks/useUser'; import { useCurrentUser } from '@/react/hooks/useUser';
import { UploadLicenseDialog } from './UploadLicenseDialog'; import { UploadLicenseDialog } from './UploadLicenseDialog';
import { LoadingDialog } from './LoadingDialog'; import { LoadingDialog } from './LoadingDialog';
@ -10,7 +10,7 @@ import { GetLicenseDialog } from './GetLicenseDialog';
type Step = 'uploadLicense' | 'loading' | 'getLicense'; type Step = 'uploadLicense' | 'loading' | 'getLicense';
export function UpgradeDialog({ onDismiss }: { onDismiss: () => void }) { export function UpgradeDialog({ onDismiss }: { onDismiss: () => void }) {
const { isAdmin } = useUser(); const { isPureAdmin } = useCurrentUser();
const [currentStep, setCurrentStep] = useState<Step>('uploadLicense'); const [currentStep, setCurrentStep] = useState<Step>('uploadLicense');
const [isGetLicenseSubmitted, setIsGetLicenseSubmitted] = useState(false); const [isGetLicenseSubmitted, setIsGetLicenseSubmitted] = useState(false);
const component = getDialog(); const component = getDialog();
@ -18,7 +18,7 @@ export function UpgradeDialog({ onDismiss }: { onDismiss: () => void }) {
return component; return component;
function getDialog() { function getDialog() {
if (!isAdmin) { if (!isPureAdmin) {
return <NonAdminUpgradeDialog onDismiss={onDismiss} />; return <NonAdminUpgradeDialog onDismiss={onDismiss} />;
} }