Skip to content

Commit

Permalink
Everest-1464: permissions for requests (#687)
Browse files Browse the repository at this point in the history
* fix: only call storage when allowed

* fix: restores call permissions

* fix: clusters call permissions

* fix: credentials API permissions

* fix: monitoring API permissions

* chore: remove hidden 403 errors

* fix: imports path

* fix: unit test

* chore: add permissions to useDbBackups

* chore: refactor permissions into hooks

* chore: add remaining permissions

* fix: bad query "enabled" conditional

* fix: show empty data when no permissions available
  • Loading branch information
fabio-silva committed Sep 19, 2024
1 parent 0b199a2 commit 32c85ff
Show file tree
Hide file tree
Showing 25 changed files with 202 additions and 97 deletions.
3 changes: 0 additions & 3 deletions ui/apps/everest/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ export const addApiErrorInterceptor = () => {
errorInterceptor = api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response.status === 403) {
return;
}
if (
error.response &&
error.response.status >= 400 &&
Expand Down
35 changes: 26 additions & 9 deletions ui/apps/everest/src/hooks/api/backup-storages/useBackupStorages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
editBackupStorageFn,
getBackupStoragesFn,
} from 'api/backupStorage';
import { useNamespacePermissionsForResource } from 'hooks/rbac';
import {
BackupStorage,
GetBackupStoragesPayload,
Expand All @@ -40,21 +41,37 @@ export interface BackupStoragesForNamespaceResult {
queryResult: UseQueryResult<BackupStorage[], unknown>;
}

export const useBackupStorages = (namespaces: string[]) => {
const queries = namespaces.map<
export const useBackupStorages = (
queriesParams: Array<{
namespace: string;
options?: PerconaQueryOptions<
GetBackupStoragesPayload,
unknown,
BackupStorage[]
>;
}>
) => {
const { canRead } = useNamespacePermissionsForResource('backup-storages');
const queries = queriesParams.map<
UseQueryOptions<GetBackupStoragesPayload, unknown, BackupStorage[]>
>((namespace) => ({
queryKey: [BACKUP_STORAGES_QUERY_KEY, namespace],
retry: false,
queryFn: () => getBackupStoragesFn(namespace),
refetchInterval: 5 * 1000,
}));
>(({ namespace, options }) => {
const allowed = canRead.includes(namespace);
return {
queryKey: [BACKUP_STORAGES_QUERY_KEY, namespace],
retry: false,
queryFn: () => getBackupStoragesFn(namespace),
refetchInterval: 5 * 1000,
select: allowed ? undefined : () => [],
...options,
enabled: (options?.enabled ?? true) && allowed,
};
});

const queryResults = useQueries({ queries });

const results: BackupStoragesForNamespaceResult[] = queryResults.map(
(item, i) => ({
namespace: namespaces[i],
namespace: queriesParams[i].namespace,
queryResult: item,
})
);
Expand Down
37 changes: 24 additions & 13 deletions ui/apps/everest/src/hooks/api/backups/useBackups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { mapBackupState } from 'utils/backups';
import { BackupFormData } from 'pages/db-cluster-details/backups/on-demand-backup-modal/on-demand-backup-modal.types';
import { PerconaQueryOptions } from 'shared-types/query.types';
import { useRBACPermissions } from 'hooks/rbac';

export const BACKUPS_QUERY_KEY = 'backups';

Expand All @@ -32,23 +33,33 @@ export const useDbBackups = (
dbClusterName: string,
namespace: string,
options?: PerconaQueryOptions<GetBackupsPayload, unknown, Backup[]>
) =>
useQuery<GetBackupsPayload, unknown, Backup[]>({
) => {
const { canRead } = useRBACPermissions(
'database-cluster-backups',
`${namespace}/${dbClusterName}`
);
return useQuery<GetBackupsPayload, unknown, Backup[]>({
queryKey: [BACKUPS_QUERY_KEY, namespace, dbClusterName],
queryFn: () => getBackupsFn(dbClusterName, namespace),
select: ({ items = [] }) =>
items.map(
({ metadata: { name }, status, spec: { backupStorageName } }) => ({
name,
created: status?.created ? new Date(status.created) : null,
completed: status?.completed ? new Date(status.completed) : null,
state: status ? mapBackupState(status?.state) : BackupStatus.UNKNOWN,
dbClusterName,
backupStorageName,
})
),
select: canRead
? ({ items = [] }) =>
items.map(
({ metadata: { name }, status, spec: { backupStorageName } }) => ({
name,
created: status?.created ? new Date(status.created) : null,
completed: status?.completed ? new Date(status.completed) : null,
state: status
? mapBackupState(status?.state)
: BackupStatus.UNKNOWN,
dbClusterName,
backupStorageName,
})
)
: () => [],
...options,
enabled: (options?.enabled ?? true) && canRead,
});
};

export const useCreateBackupOnDemand = (
dbClusterName: string,
Expand Down
11 changes: 9 additions & 2 deletions ui/apps/everest/src/hooks/api/db-cluster/useDbCluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,20 @@ import { DbCluster } from 'shared-types/dbCluster.types';
import { getDbClusterFn } from 'api/dbClusterApi';
import { PerconaQueryOptions } from 'shared-types/query.types';
import cronConverter from 'utils/cron-converter';
import { useRBACPermissions } from 'hooks/rbac';

export const DB_CLUSTER_QUERY = 'dbCluster';

export const useDbCluster = (
dbClusterName: string,
namespace: string,
options?: PerconaQueryOptions<DbCluster, unknown, DbCluster>
) =>
useQuery<DbCluster, unknown, DbCluster>({
) => {
const { canRead } = useRBACPermissions(
'database-clusters',
`${namespace}/${dbClusterName}`
);
return useQuery<DbCluster, unknown, DbCluster>({
queryKey: [DB_CLUSTER_QUERY, dbClusterName],
queryFn: () => getDbClusterFn(dbClusterName, namespace),
select: (cluster) => ({
Expand All @@ -50,4 +55,6 @@ export const useDbCluster = (
},
}),
...options,
enabled: options?.enabled && canRead,
});
};
45 changes: 32 additions & 13 deletions ui/apps/everest/src/hooks/api/db-clusters/useDbClusters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import {
UseQueryResult,
} from '@tanstack/react-query';
import { getDbClustersFn } from 'api/dbClusterApi';
import {
useNamespacePermissionsForResource,
useRBACPermissions,
} from 'hooks/rbac';
import { DbCluster, GetDbClusterPayload } from 'shared-types/dbCluster.types';
import { PerconaQueryOptions } from 'shared-types/query.types';
import cronConverter from 'utils/cron-converter';
Expand Down Expand Up @@ -57,30 +61,45 @@ export const dbClustersQuerySelect = ({
export const useDbClusters = (
namespace: string,
options?: PerconaQueryOptions<GetDbClusterPayload, unknown, DbCluster[]>
) =>
useQuery({
) => {
const { canRead } = useRBACPermissions('database-clusters', `${namespace}/*`);
return useQuery({
queryKey: [DB_CLUSTERS_QUERY_KEY],
queryFn: () => getDbClustersFn(namespace),
refetchInterval: 5 * 1000,
select: dbClustersQuerySelect,
select: canRead ? dbClustersQuerySelect : () => [],
...options,
enabled: (options?.enabled ?? true) && canRead,
});
};

export const useDBClustersForNamespaces = (namespaces: string[]) => {
const queries = namespaces.map<
export const useDBClustersForNamespaces = (
queryParams: Array<{
namespace: string;
options?: PerconaQueryOptions<GetDbClusterPayload, unknown, DbCluster[]>;
}>
) => {
const { canRead } = useNamespacePermissionsForResource('database-clusters');
const queries = queryParams.map<
UseQueryOptions<GetDbClusterPayload, unknown, DbCluster[]>
>((namespace) => ({
queryKey: [DB_CLUSTERS_QUERY_KEY, namespace],
retry: false,
queryFn: () => getDbClustersFn(namespace),
refetchInterval: 5 * 1000,
select: dbClustersQuerySelect,
}));
>(({ namespace, options }) => {
const allowed = canRead.includes(namespace);
const enabled = (options?.enabled ?? true) && allowed;
return {
queryKey: [DB_CLUSTERS_QUERY_KEY, namespace],
retry: false,
queryFn: () => getDbClustersFn(namespace),
refetchInterval: 5 * 1000,
select: allowed ? dbClustersQuerySelect : () => [],
...options,
enabled,
};
});

const queryResults = useQueries({ queries });
const results: DbClusterForNamespaceResult[] = queryResults.map(
(item, i) => ({
namespace: namespaces[i],
namespace: queryParams[i].namespace,
queryResult: item,
})
);
Expand Down
13 changes: 9 additions & 4 deletions ui/apps/everest/src/hooks/api/db-engines/useDbEngines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
getOperatorsUpgradePlan,
upgradeOperator,
} from 'api/dbEngineApi';
import { useRBACPermissions } from 'hooks/rbac';

const DB_TYPE_ORDER_MAP: Record<DbEngineType, number> = {
// Lower is more important
Expand Down Expand Up @@ -108,15 +109,19 @@ export const useDbEngines = (
namespace: string,
options?: PerconaQueryOptions<GetDbEnginesPayload, unknown, DbEngine[]>,
retrieveUpgradingEngines = false
) =>
useQuery<GetDbEnginesPayload, unknown, DbEngine[]>({
) => {
const { canRead } = useRBACPermissions('database-engines', `${namespace}/*`);
return useQuery<GetDbEnginesPayload, unknown, DbEngine[]>({
queryKey: [`dbEngines_${namespace}`],
queryFn: () => getDbEnginesFn(namespace),
select: (data) => dbEnginesQuerySelect(data, retrieveUpgradingEngines),
enabled: !!namespace,
select: canRead
? (data) => dbEnginesQuerySelect(data, retrieveUpgradingEngines)
: () => [],
retry: 2,
...options,
enabled: !!namespace && (options?.enabled ?? true) && canRead,
});
};

export const useOperatorUpgrade = (
namespace: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ import {
deleteMonitoringInstanceFn,
updateMonitoringInstanceFn,
} from 'api/monitoring';
import { useNamespacePermissionsForResource } from 'hooks/rbac';
import {
CreateMonitoringInstancePayload,
MonitoringInstance,
MonitoringInstanceList,
UpdateMonitoringInstancePayload,
} from 'shared-types/monitoring.types';
import { PerconaQueryOptions } from 'shared-types/query.types';

type HookUpdateParam = {
instanceName: string;
Expand All @@ -45,23 +47,37 @@ export interface MonitoringInstanceForNamespaceResult {
}

export const useMonitoringInstancesList = (
namespaces: string[],
enabled?: boolean
queryParams: Array<{
namespace: string;
options?: PerconaQueryOptions<
MonitoringInstanceList,
unknown,
MonitoringInstance[]
>;
}>
) => {
const queries = namespaces.map<
const { canRead } = useNamespacePermissionsForResource(
'monitoring-instances'
);
const queries = queryParams.map<
UseQueryOptions<MonitoringInstanceList, unknown, MonitoringInstance[]>
>((namespace) => ({
queryKey: [MONITORING_INSTANCES_QUERY_KEY, namespace],
retry: false,
queryFn: () => getMonitoringInstancesFn(namespace),
refetchInterval: 5 * 1000,
enabled,
}));
>(({ namespace, options }) => {
const allowed = canRead.includes(namespace);
return {
queryKey: [MONITORING_INSTANCES_QUERY_KEY, namespace],
retry: false,
queryFn: () => getMonitoringInstancesFn(namespace),
refetchInterval: 5 * 1000,
select: allowed ? undefined : () => [],
...options,
enabled: (options?.enabled ?? true) && allowed,
};
});
const queryResults = useQueries({ queries });

const results: MonitoringInstanceForNamespaceResult[] = queryResults.map(
(item, i) => ({
namespace: namespaces[i],
namespace: queryParams[i].namespace,
queryResult: item,
})
);
Expand Down
36 changes: 24 additions & 12 deletions ui/apps/everest/src/hooks/api/restores/useDbClusterRestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import {
deleteRestore,
getDbClusterRestores,
} from 'api/restores';
import { useRBACPermissions } from 'hooks/rbac';
import { generateShortUID } from 'pages/database-form/database-form-body/steps/first/utils';
import { PerconaQueryOptions } from 'shared-types/query.types';
import { GetRestorePayload, Restore } from 'shared-types/restores.types';

export const RESTORES_QUERY_KEY = 'restores';
Expand Down Expand Up @@ -97,22 +99,32 @@ export const useDbClusterRestoreFromPointInTime = (

export const useDbClusterRestores = (
namespace: string,
dbClusterName: string
) =>
useQuery<GetRestorePayload, unknown, Restore[]>({
dbClusterName: string,
options?: PerconaQueryOptions<GetRestorePayload, unknown, Restore[]>
) => {
const { canRead } = useRBACPermissions(
'database-cluster-restores',
`${namespace}/${dbClusterName}`
);
return useQuery<GetRestorePayload, unknown, Restore[]>({
queryKey: [RESTORES_QUERY_KEY, namespace, dbClusterName],
queryFn: () => getDbClusterRestores(namespace, dbClusterName),
refetchInterval: 5 * 1000,
select: (data) =>
data.items.map((item) => ({
name: item.metadata.name,
startTime: item.metadata.creationTimestamp,
endTime: item.status.completed,
state: item.status.state || 'unknown',
type: item.spec.dataSource.pitr ? 'pitr' : 'full',
backupSource: item.spec.dataSource.dbClusterBackupName || '',
})),
select: canRead
? (data) =>
data.items.map((item) => ({
name: item.metadata.name,
startTime: item.metadata.creationTimestamp,
endTime: item.status.completed,
state: item.status.state || 'unknown',
type: item.spec.dataSource.pitr ? 'pitr' : 'full',
backupSource: item.spec.dataSource.dbClusterBackupName || '',
}))
: () => [],
...options,
enabled: (options?.enabled ?? true) && canRead,
});
};

export const useDeleteRestore = (
namespace: string,
Expand Down
1 change: 0 additions & 1 deletion ui/apps/everest/src/hooks/rbac/rbac.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// import { useNamespaces } from 'hooks/api/namespaces';
import { useNamespaces } from 'hooks/api/namespaces';
import { useCallback, useEffect, useState } from 'react';
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ export const Monitoring = () => {
'monitoring-instances',
`${selectedNamespace}/*`
);

const mode = useDatabasePageMode();
const { mutate: createMonitoringInstance, isPending: creatingInstance } =
useCreateMonitoringInstance();
const { setValue } = useFormContext();

const monitoringInstancesResult = useMonitoringInstancesList([
selectedNamespace,
{
namespace: selectedNamespace,
},
]);

const monitoringInstancesLoading = monitoringInstancesResult.some(
Expand Down
Loading

0 comments on commit 32c85ff

Please sign in to comment.