Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
904d2c5
Add user profile service and APIs
thomheymann Feb 4, 2022
5a3b9f7
Added suggestions from code review
thomheymann Feb 8, 2022
fa9951d
Merge branch 'main' of github.com:elastic/kibana into user-profile-se…
thomheymann Feb 8, 2022
17055fe
Added unit tests
thomheymann Feb 10, 2022
2575ad6
Merge branch 'main' of github.com:elastic/kibana into feature/user-pr…
thomheymann Feb 10, 2022
f8f1a87
Merge branch 'main' into feature/user-profile
azasypkin Feb 14, 2022
72994c8
Merge branch 'main' into feature/user-profile
azasypkin Feb 14, 2022
1e90fa4
Activate user profile on login. (#124552)
azasypkin Feb 15, 2022
3bced57
Merge branch 'main' of github.com:elastic/kibana into feature/user-pr…
thomheymann Mar 7, 2022
54c4b9f
Merge branch 'main' of github.com:elastic/kibana into feature/user-pr…
thomheymann Apr 20, 2022
0897928
Merge branch 'main' of github.com:elastic/kibana into feature/user-pr…
thomheymann May 3, 2022
fb9f152
Merge branch 'main' into feature/user-profile
azasypkin May 11, 2022
4fc7f41
Merge branch 'main' into feature/user-profile
azasypkin May 17, 2022
1849d6b
Merge branch 'main' into feature/user-profile
azasypkin May 18, 2022
6e6fe98
Merge branch 'main' into feature/user-profile
azasypkin May 19, 2022
169d503
Merge branch 'main' into feature/user-profile
azasypkin May 23, 2022
1775421
Redesigned user profile page (#127624)
thomheymann May 24, 2022
1781dd1
Merge branch 'main' of github.com:elastic/kibana into feature/user-pr…
thomheymann May 24, 2022
719d056
Merge branch 'main' into feature/user-profile
thomheymann May 24, 2022
d8f8a3f
Merge branch 'main' into feature/user-profile
azasypkin May 25, 2022
507cd4d
Merge branch 'main' into feature/user-profile
thomheymann May 30, 2022
fc30ec9
design feedback
thomheymann May 31, 2022
f3a0606
Merge branch 'feature/user-profile' of github.com:elastic/kibana into…
thomheymann May 31, 2022
00875a2
Merge branch 'main' into feature/user-profile
azasypkin Jun 1, 2022
fb6ab09
Fix tests.
azasypkin Jun 1, 2022
705a7d8
Prevent cloud users from updating their user profile (#132622)
azasypkin Jun 1, 2022
266b0ab
Merge branch 'main' into feature/user-profile
azasypkin Jun 3, 2022
92988cd
Review#1: change color of `None provided` to `$euiColorDisabled`, rem…
azasypkin Jun 3, 2022
e5b1af6
Review#2: Get rid of KibanaPageTemplate.
azasypkin Jun 3, 2022
8f12371
Merge branch 'main' into feature/user-profile
azasypkin Jun 7, 2022
62f9a50
Merge branch 'main' into feature/user-profile
azasypkin Jun 8, 2022
6442c8c
Review#4: handle latest review comments.
azasypkin Jun 8, 2022
e4d4038
Merge branch 'main' into feature/user-profile
azasypkin Jun 8, 2022
1721b27
Increase limits.
azasypkin Jun 8, 2022
b25e83d
Merge branch 'main' into feature/user-profile
azasypkin Jun 8, 2022
3872c46
Merge branch 'main' into feature/user-profile
azasypkin Jun 9, 2022
dda0a10
Review#5: use `disabledText` color for `None provided` label.
azasypkin Jun 9, 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 @@ -288,6 +288,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 @@ -52,7 +52,7 @@ pageLoadAssetSize:
savedObjectsTagging: 59482
savedObjectsTaggingOss: 20590
searchprofiler: 67080
security: 95864
security: 115240
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 @@ -115,6 +115,7 @@
"groupName": "platform security modules",
"matchPackageNames": [
"node-forge",
"formik",
"@types/node-forge",
"require-in-the-middle",
"tough-cookie",
Expand Down
7 changes: 2 additions & 5 deletions x-pack/plugins/cloud/public/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,10 @@ describe('Cloud Plugin', () => {
expect(hashId1).not.toEqual(hashId2);
});

test('user hash does not include cloudId when authenticated via Cloud SAML', async () => {
test('user hash does not include cloudId when user is an Elastic Cloud user', async () => {
const { coreSetup } = await setupPlugin({
config: { id: 'cloudDeploymentId' },
currentUserProps: {
username,
authentication_realm: { type: 'saml', name: 'cloud-saml-kibana' },
},
currentUserProps: { username, elastic_cloud_user: true },
});

expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled();
Expand Down
6 changes: 1 addition & 5 deletions x-pack/plugins/cloud/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,11 +262,7 @@ export class CloudPlugin implements Plugin<CloudSetup> {
name: 'cloud_user_id',
context$: from(security.authc.getCurrentUser()).pipe(
map((user) => {
if (
getIsCloudEnabled(cloudId) &&
user.authentication_realm?.type === 'saml' &&
user.authentication_realm?.name === 'cloud-saml-kibana'
) {
if (user.elastic_cloud_user) {
// If the user is managed by ESS, use the plain username as the user ID:
// The username is expected to be unique for these users,
// and it matches how users are identified in the Cloud UI, so it allows us to correlate them.
Expand Down
70 changes: 70 additions & 0 deletions x-pack/plugins/cloud/server/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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 { coreMock } from '@kbn/core/server/mocks';
import { CloudPlugin } from './plugin';
import { config } from './config';
import { securityMock } from '@kbn/security-plugin/server/mocks';
import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/server/mocks';

describe('Cloud Plugin', () => {
describe('#setup', () => {
describe('setupSecurity', () => {
it('properly handles missing optional Security dependency if Cloud ID is NOT set.', async () => {
const plugin = new CloudPlugin(
coreMock.createPluginInitializerContext(config.schema.validate({}))
);

expect(() =>
plugin.setup(coreMock.createSetup(), {
usageCollection: usageCollectionPluginMock.createSetupContract(),
})
).not.toThrow();
});

it('properly handles missing optional Security dependency if Cloud ID is set.', async () => {
const plugin = new CloudPlugin(
coreMock.createPluginInitializerContext(config.schema.validate({ id: 'my-cloud' }))
);

expect(() =>
plugin.setup(coreMock.createSetup(), {
usageCollection: usageCollectionPluginMock.createSetupContract(),
})
).not.toThrow();
});

it('does not notify Security plugin about Cloud environment if Cloud ID is NOT set.', async () => {
const plugin = new CloudPlugin(
coreMock.createPluginInitializerContext(config.schema.validate({}))
);

const securityDependencyMock = securityMock.createSetup();
plugin.setup(coreMock.createSetup(), {
security: securityDependencyMock,
usageCollection: usageCollectionPluginMock.createSetupContract(),
});

expect(securityDependencyMock.setIsElasticCloudDeployment).not.toHaveBeenCalled();
});

it('properly notifies Security plugin about Cloud environment if Cloud ID is set.', async () => {
const plugin = new CloudPlugin(
coreMock.createPluginInitializerContext(config.schema.validate({ id: 'my-cloud' }))
);

const securityDependencyMock = securityMock.createSetup();
plugin.setup(coreMock.createSetup(), {
security: securityDependencyMock,
usageCollection: usageCollectionPluginMock.createSetupContract(),
});

expect(securityDependencyMock.setIsElasticCloudDeployment).toHaveBeenCalledTimes(1);
});
});
});
});
4 changes: 4 additions & 0 deletions x-pack/plugins/cloud/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export class CloudPlugin implements Plugin<CloudSetup> {
registerCloudDeploymentIdAnalyticsContext(core.analytics, this.config.id);
registerCloudUsageCollector(usageCollection, { isCloudEnabled });

if (isCloudEnabled) {
security?.setIsElasticCloudDeployment();
}

if (this.config.full_story.enabled) {
registerFullstoryRoute({
httpResources: core.http.resources,
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,
UserAvatarData,
UserInfo,
ApiKey,
UserRealm,
} from './model';
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function mockAuthenticatedUser(user: MockAuthenticatedUserProps = {}) {
lookup_realm: { name: 'native1', type: 'native' },
authentication_provider: { type: 'basic', name: 'basic1' },
authentication_type: 'realm',
elastic_cloud_user: false,
metadata: { _reserved: false },
...user,
};
Expand Down
122 changes: 121 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,128 @@
* 2.0.
*/

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

import type { AuthenticatedUser } from './authenticated_user';
import { canUserChangePassword } from './authenticated_user';
import {
canUserChangeDetails,
canUserChangePassword,
canUserHaveProfile,
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('canUserHaveProfile', () => {
it('anonymous users cannot have profiles', () => {
expect(
canUserHaveProfile(
mockAuthenticatedUser({
authentication_provider: { type: 'anonymous', name: 'basic1' },
})
)
).toBe(false);
});

it('proxy authenticated users cannot have profiles', () => {
expect(
canUserHaveProfile(
mockAuthenticatedUser({
authentication_provider: { type: 'http', name: '__http__' },
})
)
).toBe(false);
});

it('non-anonymous users that can have sessions can have profiles', () => {
for (const providerType of ['saml', 'oidc', 'basic', 'token', 'pki', 'kerberos']) {
expect(
canUserHaveProfile(
mockAuthenticatedUser({
authentication_provider: { type: providerType, name: `${providerType}_name` },
})
)
).toBe(true);
}
});
});

describe('#canUserChangePassword', () => {
['reserved', 'native'].forEach((realm) => {
Expand Down
43 changes: 41 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,13 +5,25 @@
* 2.0.
*/

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

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

const REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE = ['reserved', 'native'];

/**
* An Elasticsearch realm that was used to resolve and authenticate the user.
*/
export interface UserRealm {
/**
* Arbitrary name of the security realm.
*/
name: string;

/**
* Type of the security realm (file, native, saml etc.).
*/
type: string;
}

Expand Down Expand Up @@ -40,11 +52,38 @@ export interface AuthenticatedUser extends User {
* @example "realm" | "api_key" | "token" | "anonymous" | "internal"
*/
authentication_type: string;

/**
* Indicates whether user is authenticated via Elastic Cloud built-in SAML realm.
*/
elastic_cloud_user: boolean;
}

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

export function canUserChangePassword(user: AuthenticatedUser) {
/**
* All users are supposed to have profiles except anonymous users and users authenticated
* via authentication HTTP proxies.
* @param user Authenticated user information.
*/
export function canUserHaveProfile(user: AuthenticatedUser) {
return !isUserAnonymous(user) && user.authentication_provider.type !== 'http';
}

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;
}
19 changes: 18 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,26 @@

export type { ApiKey, ApiKeyToInvalidate, ApiKeyRoleDescriptors } from './api_key';
export type { User, EditUser } from './user';
export type {
AuthenticatedUserProfile,
UserProfile,
UserData,
UserInfo,
UserAvatarData,
} 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,
canUserHaveProfile,
} 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;
}
Loading