Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,20 @@ describe('useGetUserSettingConfigMap', () => {

describe('user identification scenarios', () => {
it('should use impersonated user name when present', () => {
const impersonateUserInfo = {
impersonateName: 'impersonate-user',
uid: 'test-uid',
username: 'test-username',
};

useSelectorMock.mockReturnValue(impersonateUserInfo);
useSelectorMock.mockImplementation((selector) =>
selector({
sdkCore: {
user: { uid: 'test-uid', username: 'test-username' },
impersonate: { name: 'impersonate-user' },
},
}),
);
useK8sWatchResourceMock.mockReturnValue([mockConfigMapData, true, null]);

const { result } = renderHook(() => useGetUserSettingConfigMap());

// Should use impersonate name directly and return config map data
expect(result.current).toEqual([mockConfigMapData, true, null]);

// Verify the correct resource spec was passed to useK8sWatchResource
expect(useK8sWatchResourceMock).toHaveBeenCalledWith({
kind: ConfigMapModel.kind,
namespace: USER_SETTING_CONFIGMAP_NAMESPACE,
Expand All @@ -68,73 +67,76 @@ describe('useGetUserSettingConfigMap', () => {
});
});

it('should use uid when no impersonation and uid is available', () => {
const userInfoWithUid = {
impersonateName: null,
uid: 'test-uid-123',
username: 'test-username',
};

useSelectorMock.mockReturnValue(userInfoWithUid);
it('should use hashed username when no impersonation and uid is available', () => {
useSelectorMock.mockImplementation((selector) =>
selector({
sdkCore: {
user: { uid: 'test-uid-123', username: 'test-username' },
},
}),
);
useK8sWatchResourceMock.mockReturnValue([mockConfigMapData, true, null]);

const { result } = renderHook(() => useGetUserSettingConfigMap());

// Should use uid directly and return config map data
expect(result.current).toEqual([mockConfigMapData, true, null]);

// Verify the correct resource spec was passed
// SHA256("test-username")
expect(useK8sWatchResourceMock).toHaveBeenCalledWith({
kind: ConfigMapModel.kind,
namespace: USER_SETTING_CONFIGMAP_NAMESPACE,
isList: false,
name: 'user-settings-test-uid-123',
name: 'user-settings-70609918baaace3eb22057bbae6dfbd7d0d2c34eaeecd5968ef455a64caee242',
});
});

it('should handle empty user info gracefully', () => {
const emptyUserInfo = {
impersonateName: null,
uid: null,
username: null,
};

useSelectorMock.mockReturnValue(emptyUserInfo);
useSelectorMock.mockImplementation((selector) =>
selector({
sdkCore: {
user: {},
},
}),
);

const { result } = renderHook(() => useGetUserSettingConfigMap());

// Should return null config map resource when no user identifier
expect(result.current).toEqual([null, true, null]);

// Verify null resource was passed to useK8sWatchResource
expect(useK8sWatchResourceMock).toHaveBeenCalledWith(null);
});

it('should handle username-only scenario', () => {
const userInfoWithUsername = {
impersonateName: null,
uid: null,
username: 'test-username',
};

useSelectorMock.mockReturnValue(userInfoWithUsername);
useSelectorMock.mockImplementation((selector) =>
selector({
sdkCore: {
user: { username: 'test-username' },
},
}),
);
useK8sWatchResourceMock.mockReturnValue([mockConfigMapData, true, null]);

const { result } = renderHook(() => useGetUserSettingConfigMap());

// Initially, should return null since hashing is async
expect(result.current[0]).toBeNull();
// SHA256("test-username") - sync, no longer async
expect(result.current).toEqual([mockConfigMapData, true, null]);
expect(useK8sWatchResourceMock).toHaveBeenCalledWith({
kind: ConfigMapModel.kind,
namespace: USER_SETTING_CONFIGMAP_NAMESPACE,
isList: false,
name: 'user-settings-70609918baaace3eb22057bbae6dfbd7d0d2c34eaeecd5968ef455a64caee242',
});
});
});

describe('integration with useK8sWatchResource', () => {
it('should pass through loading state', () => {
const userInfoWithUid = {
impersonateName: null,
uid: 'test-uid',
username: 'test-username',
};

useSelectorMock.mockReturnValue(userInfoWithUid);
useSelectorMock.mockImplementation((selector) =>
selector({
sdkCore: {
user: { uid: 'test-uid', username: 'test-username' },
},
}),
);
useK8sWatchResourceMock.mockReturnValue([null, false, null]);

const { result } = renderHook(() => useGetUserSettingConfigMap());
Expand All @@ -143,14 +145,15 @@ describe('useGetUserSettingConfigMap', () => {
});

it('should pass through error state', () => {
const userInfoWithUid = {
impersonateName: null,
uid: 'test-uid',
username: 'test-username',
};
useSelectorMock.mockImplementation((selector) =>
selector({
sdkCore: {
user: { uid: 'test-uid', username: 'test-username' },
},
}),
);

const error = new Error('K8s API error');
useSelectorMock.mockReturnValue(userInfoWithUid);
useK8sWatchResourceMock.mockReturnValue([null, false, error]);

const { result } = renderHook(() => useGetUserSettingConfigMap());
Expand All @@ -159,13 +162,13 @@ describe('useGetUserSettingConfigMap', () => {
});

it('should pass through successful data', () => {
const userInfoWithUid = {
impersonateName: null,
uid: 'test-uid',
username: 'test-username',
};

useSelectorMock.mockReturnValue(userInfoWithUid);
useSelectorMock.mockImplementation((selector) =>
selector({
sdkCore: {
user: { uid: 'test-uid', username: 'test-username' },
},
}),
);
useK8sWatchResourceMock.mockReturnValue([mockConfigMapData, true, null]);

const { result } = renderHook(() => useGetUserSettingConfigMap());
Expand All @@ -174,39 +177,38 @@ describe('useGetUserSettingConfigMap', () => {
});

it('should create ConfigMap resource with correct parameters', () => {
const userInfo = {
impersonateName: null,
uid: 'test-uid-456',
username: 'test-user',
};

useSelectorMock.mockReturnValue(userInfo);
useSelectorMock.mockImplementation((selector) =>
selector({
sdkCore: {
user: { uid: 'test-uid-456', username: 'test-user' },
},
}),
);
useK8sWatchResourceMock.mockReturnValue([mockConfigMapData, true, null]);

renderHook(() => useGetUserSettingConfigMap());

// Verify useK8sWatchResource was called with correct resource spec
// SHA256("test-user")
expect(useK8sWatchResourceMock).toHaveBeenCalledWith({
kind: ConfigMapModel.kind,
namespace: USER_SETTING_CONFIGMAP_NAMESPACE,
isList: false,
name: 'user-settings-test-uid-456',
name: 'user-settings-f85ac825d102b9f2d546aa1679ea991ae845994c1343730d564f3fcd0a2168c3',
});
});

it('should use null resource when no user identifier is available', () => {
const emptyUserInfo = {
impersonateName: null,
uid: null,
username: '',
};

useSelectorMock.mockReturnValue(emptyUserInfo);
useSelectorMock.mockImplementation((selector) =>
selector({
sdkCore: {
user: {},
},
}),
);
useK8sWatchResourceMock.mockReturnValue([null, true, null]);

const { result } = renderHook(() => useGetUserSettingConfigMap());

// Should call useK8sWatchResource with null
expect(useK8sWatchResourceMock).toHaveBeenCalledWith(null);
expect(result.current).toEqual([null, true, null]);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const emptyConfigMap: ConfigMapKind = {
apiVersion: 'v1',
kind: 'ConfigMap',
metadata: {
name: `user-settings-1234`,
name: `user-settings-ae5deb822e0d71992900471a7199d0d95b8e7c9d05c40a8245a281fd2c1d6684`,
namespace: USER_SETTING_CONFIGMAP_NAMESPACE,
},
};
Expand All @@ -61,7 +61,9 @@ const savedDataConfigMap: ConfigMapKind = {

beforeEach(() => {
jest.resetAllMocks();
useSelectorMock.mockImplementation((selector) => selector({ sdkCore: { user: { uid: 'foo' } } }));
useSelectorMock.mockImplementation((selector) =>
selector({ sdkCore: { user: { uid: 'foo', username: 'testuser' } } }),
);
useFavoritesOptionsMock.mockReturnValue([[], jest.fn(), true]);

// eslint-disable-next-line no-console
Expand Down
Original file line number Diff line number Diff line change
@@ -1,53 +1,18 @@
import { useMemo, useState, useEffect } from 'react';
import { shallowEqual } from 'react-redux';
import { useMemo } from 'react';
import type { K8sResourceKind } from '@console/dynamic-plugin-sdk/src';
import { getImpersonate, getUser } from '@console/dynamic-plugin-sdk/src';
import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook';
import { ConfigMapModel } from '@console/internal/models';
import { useConsoleSelector } from '@console/shared/src/hooks/useConsoleSelector';
import { USER_SETTING_CONFIGMAP_NAMESPACE } from '../utils/user-settings';
import { hashUsernameForSettings, USER_SETTING_CONFIGMAP_NAMESPACE } from '../utils/user-settings';

export const useGetUserSettingConfigMap = () => {
const [hashedUsername, setHashedUsername] = useState<string | null>(null);

const hashNameOrKubeadmin = async (name: string): Promise<string | null> => {
if (!name) {
return null;
}

if (name === 'kube:admin') {
return 'kubeadmin';
}

const encoder = new TextEncoder();
const data = encoder.encode(name);
const hashBuffer = await window.crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
};

// User and impersonate info
const userInfo = useConsoleSelector((state) => {
const userUid = useConsoleSelector((state) => {
const impersonateName = getImpersonate(state)?.name;
const { uid, username } = getUser(state) ?? {};
return { impersonateName, uid, username };
}, shallowEqual);

// Hash the username asynchronously
useEffect(() => {
if (userInfo.username) {
hashNameOrKubeadmin(userInfo.username)
.then(setHashedUsername)
.catch(() => {
setHashedUsername(null);
});
} else {
setHashedUsername(null);
}
}, [userInfo.username]);

// Compute the final user UID
const userUid = userInfo.impersonateName || userInfo.uid || hashedUsername || '';
const hashName = hashUsernameForSettings(username, uid);
return impersonateName || hashName || '';
});

const configMapResource = useMemo(
() =>
Expand Down
19 changes: 3 additions & 16 deletions frontend/packages/console-shared/src/hooks/useUserPreference.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { createHash } from 'crypto';
import type { SetStateAction } from 'react';
import { useRef, useCallback, useEffect, useState, useMemo } from 'react';
import type { UseUserPreference } from '@console/dynamic-plugin-sdk';
Expand All @@ -10,6 +9,7 @@ import { useConsoleSelector } from '@console/shared/src/hooks/useConsoleSelector
import {
createConfigMap,
deserializeData,
hashUsernameForSettings,
seralizeData,
updateConfigMap,
USER_SETTING_CONFIGMAP_NAMESPACE,
Expand Down Expand Up @@ -70,25 +70,12 @@ export const useUserPreference: UseUserPreference = <T>(
// Request counter
const [isRequestPending, increaseRequest, decreaseRequest] = useCounterRef();

const hashNameOrKubeadmin = (name: string): string | null => {
if (!name) {
return null;
}

if (name === 'kube:admin') {
return 'kubeadmin';
}
const hash = createHash('sha256');
hash.update(name);
return hash.digest('hex');
};

// User and impersonate
const userUid = useConsoleSelector((state) => {
const impersonateName = getImpersonate(state)?.name;
const { uid, username } = getUser(state) ?? {};
const hashName = hashNameOrKubeadmin(username);
return impersonateName || uid || hashName || '';
const hashName = hashUsernameForSettings(username, uid);
return impersonateName || hashName || '';
});

const impersonate: boolean = useConsoleSelector((state) => !!getImpersonate(state));
Expand Down
13 changes: 13 additions & 0 deletions frontend/packages/console-shared/src/utils/user-settings.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import { createHash } from 'crypto';
import { resourceURL } from '@console/dynamic-plugin-sdk/src/utils/k8s';
import { ConfigMapModel } from '@console/internal/models';
import type { ConfigMapKind } from '@console/internal/module/k8s/types';
import { coFetch } from '@console/shared/src/utils/console-fetch';

export const USER_SETTING_CONFIGMAP_NAMESPACE = 'openshift-console-user-settings';

export const hashUsernameForSettings = (name: string, uid?: string): string | null => {
if (!name) {
return null;
}
if (name === 'kube:admin' && !uid) {
return 'kubeadmin';
}
const hash = createHash('sha256');
hash.update(name);
return hash.digest('hex');
};

export const createConfigMap = async (): Promise<ConfigMapKind> => {
try {
const response = await coFetch('/api/console/user-settings', {
Expand Down
Loading