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

[Authz] Operator privileges #196583

Merged
merged 23 commits into from
Dec 12, 2024
Merged
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
34 changes: 33 additions & 1 deletion dev_docs/key_concepts/api_authorization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,38 @@ router.get({
}, handler);
```

### Configuring operator and superuser privileges
We have two special predefined privilege sets that can be used in security configuration:
1. Operator
```ts
router.get({
path: '/api/path',
security: {
authz: {
requiredPrivileges: [ReservedPrivilegesSet.operator, '<privilege_2>'],
},
},
...
}, handler);
```
Operator privileges check is enforced only if operator privileges are enabled in the Elasticsearch configuration.
For more information on operator privileges, refer to the [Operator privileges documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/operator-privileges.html).
If operator privileges are disabled, we skip the check for it, so the only privilege checked from the example above is `<privilege_2>`.
Operator privileges cannot be used as standalone, it is required to explicitly specify additional privileges in the configuration to ensure that the route is protected even when operator privileges are disabled.

2. Superuser
```ts
router.get({
path: '/api/path',
security: {
authz: {
requiredPrivileges: [ReservedPrivilegesSet.superuser],
},
},
...
}, handler);
```

### Opting out of authorization for specific routes
**Before migration:**
```ts
Expand Down Expand Up @@ -291,7 +323,7 @@ GET /api/oas?pathStartsWith=/your/api/path
## Migrating from `access` tags to `security` configuration
We aim to use the same privileges that are currently defined with tags `access:<privilege_name>`.
To assist with this migration, we have created eslint rule `no_deprecated_authz_config`, that will automatically convert your `access` tags to the new `security` configuration.
It scans route definitions and converts `access` tags to the new `requiredPriviliges` configuration.
It scans route definitions and converts `access` tags to the new `requiredPrivileges` configuration.

Note: The rule is disabled by default to avoid automatic migrations without an oversight. You can perform migrations by running:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,16 @@ describe('RouteSecurity validation', () => {
expect(() => validRouteSecurity(routeSecurity)).not.toThrow();
});

it('should pass validation when operator privileges are combined with superuser', () => {
const routeSecurity = {
authz: {
requiredPrivileges: [ReservedPrivilegesSet.operator, ReservedPrivilegesSet.superuser],
},
};

expect(() => validRouteSecurity(routeSecurity)).not.toThrow();
});

it('should fail validation when anyRequired and allRequired have the same values', () => {
const invalidRouteSecurity = {
authz: {
Expand Down Expand Up @@ -263,6 +273,20 @@ describe('RouteSecurity validation', () => {
);
});

it('should fail validation when allRequired has duplicate entries', () => {
const invalidRouteSecurity = {
authz: {
requiredPrivileges: [
{ anyRequired: ['privilege4', 'privilege5'], allRequired: ['privilege1', 'privilege1'] },
],
},
};

expect(() => validRouteSecurity(invalidRouteSecurity)).toThrowErrorMatchingInlineSnapshot(
`"[authz.requiredPrivileges]: allRequired privileges must contain unique values"`
);
});

it('should fail validation when anyRequired has duplicates in multiple privilege entries', () => {
const invalidRouteSecurity = {
authz: {
Expand All @@ -289,7 +313,7 @@ describe('RouteSecurity validation', () => {
};

expect(() => validRouteSecurity(invalidRouteSecurity)).toThrowErrorMatchingInlineSnapshot(
`"[authz.requiredPrivileges]: Combining superuser with other privileges is redundant, superuser privileges set can be only used as a standalone privilege."`
`"[authz.requiredPrivileges]: Using superuser privileges in anyRequired is not allowed"`
);
});

Expand All @@ -304,4 +328,41 @@ describe('RouteSecurity validation', () => {
`"[authz.requiredPrivileges]: Combining superuser with other privileges is redundant, superuser privileges set can be only used as a standalone privilege."`
);
});

it('should fail validation when anyRequired has operator privileges set', () => {
const invalidRouteSecurity = {
authz: {
requiredPrivileges: [
{ anyRequired: ['privilege1', 'privilege2'], allRequired: ['privilege4'] },
{ anyRequired: ['privilege5', ReservedPrivilegesSet.operator] },
],
},
};

expect(() => validRouteSecurity(invalidRouteSecurity)).toThrowErrorMatchingInlineSnapshot(
`"[authz.requiredPrivileges]: Using operator privileges in anyRequired is not allowed"`
);
});

it('should fail validation when operator privileges set is used as standalone', () => {
expect(() =>
validRouteSecurity({
authz: {
requiredPrivileges: [{ allRequired: [ReservedPrivilegesSet.operator] }],
},
})
).toThrowErrorMatchingInlineSnapshot(
`"[authz.requiredPrivileges]: Operator privilege requires at least one additional non-operator privilege to be defined"`
);

expect(() =>
validRouteSecurity({
authz: {
requiredPrivileges: [ReservedPrivilegesSet.operator],
},
})
).toThrowErrorMatchingInlineSnapshot(
`"[authz.requiredPrivileges]: Operator privilege requires at least one additional non-operator privilege to be defined"`
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,33 @@ const requiredPrivilegesSchema = schema.arrayOf(
}
});

if (anyRequired.includes(ReservedPrivilegesSet.superuser)) {
return 'Using superuser privileges in anyRequired is not allowed';
}

const hasSuperuserInAllRequired = allRequired.includes(ReservedPrivilegesSet.superuser);
const hasOperatorInAllRequired = allRequired.includes(ReservedPrivilegesSet.operator);

// Combining superuser with other privileges is redundant.
// If user is a superuser, they inherently have access to all the privileges that may come with other roles.
// The exception is when superuser and operator are the only required privileges.
if (
anyRequired.includes(ReservedPrivilegesSet.superuser) ||
(allRequired.includes(ReservedPrivilegesSet.superuser) && allRequired.length > 1)
hasSuperuserInAllRequired &&
allRequired.length > 1 &&
!(hasOperatorInAllRequired && allRequired.length === 2)
) {
return 'Combining superuser with other privileges is redundant, superuser privileges set can be only used as a standalone privilege.';
}

// Operator privilege requires at least one additional non-operator privilege to be defined, that's why it's not allowed in anyRequired.
if (anyRequired.includes(ReservedPrivilegesSet.operator)) {
elena-shostak marked this conversation as resolved.
Show resolved Hide resolved
return 'Using operator privileges in anyRequired is not allowed';
}

if (hasOperatorInAllRequired && allRequired.length === 1) {
return 'Operator privilege requires at least one additional non-operator privilege to be defined';
}

if (anyRequired.length && allRequired.length) {
for (const privilege of anyRequired) {
if (allRequired.includes(privilege)) {
Expand All @@ -68,12 +86,20 @@ const requiredPrivilegesSchema = schema.arrayOf(
}

if (anyRequired.length) {
const uniquePrivileges = new Set([...anyRequired]);
const uniqueAnyPrivileges = new Set([...anyRequired]);

if (anyRequired.length !== uniquePrivileges.size) {
if (anyRequired.length !== uniqueAnyPrivileges.size) {
return 'anyRequired privileges must contain unique values';
}
}

if (allRequired.length) {
const uniqueAllPrivileges = new Set([...allRequired]);

if (allRequired.length !== uniqueAllPrivileges.size) {
return 'allRequired privileges must contain unique values';
}
}
},
minSize: 1,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,9 @@ export interface AuthenticatedUser extends User {
* User profile ID of this user.
*/
profile_uid?: string;

/**
* Indicates whether user is an operator.
*/
operator?: boolean;
}
1 change: 1 addition & 0 deletions x-pack/packages/security/plugin_types_server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type {
CheckUserProfilesPrivileges,
AuthorizationMode,
AuthorizationServiceSetup,
EsSecurityConfig,
} from './src/authorization';
export type { SecurityPluginSetup, SecurityPluginStart } from './src/plugin';
export type {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* 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 type { Client } from '@elastic/elasticsearch';

type XpackUsageResponse = Awaited<ReturnType<Client['xpack']['usage']>>;
type XpackUsageSecurity = XpackUsageResponse['security'];

export type EsSecurityConfig = Pick<XpackUsageSecurity, 'operator_privileges'>;
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export type {
PrivilegeDeprecationsRolesByFeatureIdResponse,
} from './deprecations';
export type { AuthorizationMode } from './mode';
export type { EsSecurityConfig } from './es_security_config';

export { GLOBAL_RESOURCE } from './constants';
export { elasticsearchRoleSchema, getKibanaRoleSchema } from './role_schema';
2 changes: 1 addition & 1 deletion x-pack/packages/security/plugin_types_server/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export interface SecurityPluginStart {
/**
* Authorization services to manage and access the permissions a particular user has.
*/
authz: AuthorizationServiceSetup;
authz: Omit<AuthorizationServiceSetup, 'getCurrentUser' | 'getSecurityConfig'>;
/**
* User profiles services to retrieve user profiles.
*
Expand Down
Loading
Loading