Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redesigned user profile page #127624

Merged
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
2e3a7b1
Add user profile UI
thomheymann Mar 14, 2022
98eb7c7
add code doc
thomheymann Mar 14, 2022
25eaf0b
More
thomheymann Mar 14, 2022
87ae3f1
Fix nav control for anonymous users
thomheymann Mar 15, 2022
e696ce0
Move presentational logic outside form row
thomheymann Mar 15, 2022
10f8f9b
Fix disabled state and update profile api
thomheymann Mar 15, 2022
51825b9
remove presentational logic from form row
thomheymann Mar 16, 2022
6147fb5
wip
thomheymann Mar 16, 2022
f46f750
Fix initials
thomheymann Mar 16, 2022
4164504
.
thomheymann Mar 16, 2022
d96f416
empty user avatar
thomheymann Mar 16, 2022
1a42bf4
simplify reserved user screen
thomheymann Mar 16, 2022
b50a171
.
thomheymann Mar 17, 2022
97fe039
unit tests
thomheymann Mar 18, 2022
ea08f16
type fixes
thomheymann Mar 18, 2022
d416345
.
thomheymann Mar 21, 2022
55eab66
.
thomheymann Mar 21, 2022
488c489
.
thomheymann Mar 23, 2022
4e7e819
.
thomheymann Mar 28, 2022
31a1c35
.
thomheymann Mar 28, 2022
5ce63e6
.
thomheymann Mar 28, 2022
c58ff85
.
thomheymann Mar 28, 2022
c4c6d36
.
thomheymann Mar 28, 2022
2bd8875
.
thomheymann Mar 28, 2022
c5a4b71
.
thomheymann Mar 28, 2022
833a8c7
.
thomheymann Mar 29, 2022
c6ec506
.
thomheymann Mar 29, 2022
a265b00
.
thomheymann Mar 29, 2022
167a3d5
.
thomheymann Mar 29, 2022
9043774
.
thomheymann Mar 29, 2022
17101ee
Merge remote-tracking branch 'upstream/feature/user-profile' into fea…
thomheymann Apr 20, 2022
906b4c0
.
thomheymann Apr 20, 2022
e433179
.
thomheymann Apr 21, 2022
d6851c7
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Apr 21, 2022
cf2d7b7
.
thomheymann Apr 21, 2022
2049070
Merge branch 'feature/user-profile-ui' of github.com:thomheymann/kiba…
thomheymann Apr 21, 2022
ee80e55
.
thomheymann Apr 21, 2022
b9d8a1b
Merge branch 'feature/user-profile' of github.com:elastic/kibana into…
thomheymann May 3, 2022
b0f7015
.
thomheymann May 3, 2022
8afade3
.
thomheymann May 3, 2022
29d4ba8
.
thomheymann May 4, 2022
eb414b9
.
thomheymann May 4, 2022
7a08f59
Merge branch 'feature/user-profile' into feature/user-profile-ui
kibanamachine May 4, 2022
1ece49a
Merge branch 'feature/user-profile' into feature/user-profile-ui
kibanamachine May 6, 2022
3385e40
Merge branch 'feature/user-profile' into pr-127624-user-profiles
azasypkin May 11, 2022
02071b1
Merge branch 'feature/user-profile' into pr-127624-user-profiles
azasypkin May 17, 2022
a1b53ef
Review#1: handle UI inconsistencies, update avatar in the navigation …
azasypkin May 17, 2022
accd6c0
Fix type errors.
azasypkin May 17, 2022
cf1c0ce
Merge branch 'feature/user-profile' into pr-127624-user-profiles
azasypkin May 18, 2022
462d2c2
Use different validation model for the first and subsequent submits.
azasypkin May 18, 2022
a34e015
Review#2: move change password form to a modal, support users authent…
azasypkin May 19, 2022
a887dad
Use proper test subject for the password change Cancel button.
azasypkin May 19, 2022
81ed8d3
Review#3: retry profile activation requests.
azasypkin May 19, 2022
3522bca
Merge branch 'feature/user-profile' into pr-127624-user-profiles
azasypkin May 19, 2022
3e282fe
Review#4: align text color of the non-editable profile fields with th…
azasypkin May 19, 2022
73fce79
Merge branch 'feature/user-profile' into pr-127624-user-profiles
azasypkin May 23, 2022
0349691
Review#4: improve warning message for the change password form when c…
azasypkin May 23, 2022
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@
"file-saver": "^1.3.8",
"file-type": "^10.9.0",
"font-awesome": "4.7.0",
"formik": "^2.2.9",
"fp-ts": "^2.3.1",
"geojson-vt": "^3.2.1",
"get-port": "^5.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pageLoadAssetSize:
savedObjectsTagging: 59482
savedObjectsTaggingOss: 20590
searchprofiler: 67080
security: 95864
security: 100000
snapshotRestore: 79032
spaces: 57868
telemetry: 51957
Expand Down
1 change: 1 addition & 0 deletions renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"matchPackageNames": [
"broadcast-channel",
"node-forge",
"formik",
"@types/node-forge",
"require-in-the-middle",
"tough-cookie",
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/security/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
export type { SecurityLicense, SecurityLicenseFeatures, LoginLayout } from './licensing';
export type {
AuthenticatedUser,
AuthenticatedUserProfile,
AuthenticationProvider,
PrivilegeDeprecationsService,
PrivilegeDeprecationsRolesByFeatureIdRequest,
Expand All @@ -17,6 +18,10 @@ export type {
RoleKibanaPrivilege,
FeaturesPrivileges,
User,
UserProfile,
UserData,
UserAvatar,
UserInfo,
ApiKey,
UserRealm,
} from './model';
83 changes: 82 additions & 1 deletion x-pack/plugins/security/common/model/authenticated_user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,89 @@
* 2.0.
*/

import { applicationServiceMock } from '@kbn/core/public/mocks';

import type { AuthenticatedUser } from './authenticated_user';
import { canUserChangePassword } from './authenticated_user';
import { canUserChangeDetails, canUserChangePassword, isUserAnonymous } from './authenticated_user';
import { mockAuthenticatedUser } from './authenticated_user.mock';

describe('canUserChangeDetails', () => {
const { capabilities } = applicationServiceMock.createStartContract();

it('should indicate when user can change their details', () => {
expect(
canUserChangeDetails(
mockAuthenticatedUser({
authentication_realm: { type: 'native', name: 'native1' },
}),
{
...capabilities,
management: {
security: {
users: true,
},
},
}
)
).toBe(true);
});

it('should indicate when user cannot change their details', () => {
expect(
canUserChangeDetails(
mockAuthenticatedUser({
authentication_realm: { type: 'native', name: 'native1' },
}),
{
...capabilities,
management: {
security: {
users: false,
},
},
}
)
).toBe(false);

expect(
canUserChangeDetails(
mockAuthenticatedUser({
authentication_realm: { type: 'reserved', name: 'reserved1' },
}),
{
...capabilities,
management: {
security: {
users: true,
},
},
}
)
).toBe(false);
});
});

describe('isUserAnonymous', () => {
it('should indicate anonymous user', () => {
expect(
isUserAnonymous(
mockAuthenticatedUser({
authentication_provider: { type: 'anonymous', name: 'basic1' },
})
)
).toBe(true);
});

it('should indicate non-anonymous user', () => {
expect(
isUserAnonymous(
mockAuthenticatedUser({
authentication_provider: { type: 'basic', name: 'basic1' },
})
)
).toBe(false);
});
});

describe('#canUserChangePassword', () => {
['reserved', 'native'].forEach((realm) => {
Expand Down
19 changes: 17 additions & 2 deletions x-pack/plugins/security/common/model/authenticated_user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* 2.0.
*/

import type { Capabilities } from '@kbn/core/types';

import type { AuthenticationProvider } from './authentication_provider';
import type { User } from './user';

Expand Down Expand Up @@ -42,9 +44,22 @@ export interface AuthenticatedUser extends User {
authentication_type: string;
}

export function canUserChangePassword(user: AuthenticatedUser) {
export function isUserAnonymous(user: Pick<AuthenticatedUser, 'authentication_provider'>) {
return user.authentication_provider.type === 'anonymous';
}

export function canUserChangePassword(
user: Pick<AuthenticatedUser, 'authentication_realm' | 'authentication_provider'>
) {
return (
REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE.includes(user.authentication_realm.type) &&
user.authentication_provider.type !== 'anonymous'
!isUserAnonymous(user)
);
}

export function canUserChangeDetails(
user: Pick<AuthenticatedUser, 'authentication_realm'>,
capabilities: Capabilities
) {
return user.authentication_realm.type === 'native' && capabilities.management.security.users;
}
14 changes: 13 additions & 1 deletion x-pack/plugins/security/common/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,21 @@

export type { ApiKey, ApiKeyToInvalidate, ApiKeyRoleDescriptors } from './api_key';
export type { User, EditUser } from './user';
export type {
AuthenticatedUserProfile,
UserProfile,
UserData,
UserInfo,
UserAvatar,
} from './user_profile';
export {
getUserAvatarColor,
getUserAvatarInitials,
USER_AVATAR_MAX_INITIALS,
} from './user_profile';
export { getUserDisplayName } from './user';
export type { AuthenticatedUser, UserRealm } from './authenticated_user';
export { canUserChangePassword } from './authenticated_user';
export { canUserChangePassword, canUserChangeDetails, isUserAnonymous } from './authenticated_user';
export type { AuthenticationProvider } from './authentication_provider';
export { shouldProviderUseLoginForm } from './authentication_provider';
export type { BuiltinESPrivileges } from './builtin_es_privileges';
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/security/common/model/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ export interface EditUser extends User {
confirmPassword?: string;
}

export function getUserDisplayName(user: User) {
export function getUserDisplayName(user: Pick<User, 'username' | 'full_name'>) {
return user.full_name || user.username;
}
29 changes: 29 additions & 0 deletions x-pack/plugins/security/common/model/user_profile.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { mockAuthenticatedUser } from './authenticated_user.mock';
import type { AuthenticatedUserProfile } from './user_profile';

export const userProfileMock = {
create: (userProfile: Partial<AuthenticatedUserProfile> = {}): AuthenticatedUserProfile => {
const user = mockAuthenticatedUser({
username: 'some-username',
roles: [],
enabled: true,
});
return {
uid: 'some-profile-uid',
enabled: true,
user: {
...user,
active: true,
},
data: {},
...userProfile,
};
},
};
116 changes: 116 additions & 0 deletions x-pack/plugins/security/common/model/user_profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { VISUALIZATION_COLORS } from '@elastic/eui';

import type { User } from '..';
import type { AuthenticatedUser } from './authenticated_user';
import { getUserDisplayName } from './user';

/**
* User information returned in user profile.
*/
export interface UserInfo extends User {
active: boolean;
}

/**
* Avatar stored in user profile.
*/
export interface UserAvatar {
initials?: string;
color?: string;
imageUrl?: string;
}

/**
* Placeholder for data stored in user profile.
*/
export type UserData = Record<string, unknown>;

/**
* Describes properties stored in user profile.
*/
export interface UserProfile<T extends UserData = UserData> {
/**
* Unique ID for of the user profile.
*/
uid: string;

/**
* Indicates whether user profile is enabled or not.
*/
enabled: boolean;

/**
* Information about the user that owns profile.
*/
user: UserInfo;

/**
* User specific data associated with the profile.
*/
data: T;
}

/**
* User profile enriched with session information.
*/
export interface AuthenticatedUserProfile<T extends UserData = UserData> extends UserProfile<T> {
/**
* Information about the currently authenticated user that owns the profile.
*/
user: UserProfile['user'] & Pick<AuthenticatedUser, 'authentication_provider'>;
}

export const USER_AVATAR_FALLBACK_CODE_POINT = 97; // code point for lowercase "a"
export const USER_AVATAR_MAX_INITIALS = 2;

/**
* Determines the color for the provided user profile.
* If a color is present on the user profile itself, then that is used.
* Otherwise, a color is provided from EUI's Visualization Colors based on the display name.
*
* @param {UserInfo} user User info
* @param {UserAvatar} avatar User avatar
*/
export function getUserAvatarColor(
user: Pick<UserInfo, 'username' | 'full_name'>,
avatar?: UserAvatar
) {
if (avatar && avatar.color) {
return avatar.color;
}

const firstCodePoint = getUserDisplayName(user).codePointAt(0) || USER_AVATAR_FALLBACK_CODE_POINT;

return VISUALIZATION_COLORS[firstCodePoint % VISUALIZATION_COLORS.length];
}

/**
* Determines the initials for the provided user profile.
* If initials are present on the user profile itself, then that is used.
* Otherwise, the initials are calculated based off the words in the display name, with a max length of 2 characters.
*
* @param {UserInfo} user User info
* @param {UserAvatar} avatar User avatar
*/
export function getUserAvatarInitials(
user: Pick<UserInfo, 'username' | 'full_name'>,
avatar?: UserAvatar
) {
if (avatar && avatar.initials) {
return avatar.initials;
}

const words = getUserDisplayName(user).split(' ');
const numInitials = Math.min(USER_AVATAR_MAX_INITIALS, words.length);

words.splice(numInitials, words.length);

return words.map((word) => word.substring(0, 1)).join('');
}
Loading