Skip to content
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
42 changes: 42 additions & 0 deletions dev_docs/key_concepts/api_authorization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,48 @@ router.get({
}, handler);
```

**Example 4: Complex configuration with nested `allOf`.**
Requires (`<privilege_1>` AND `<privilege_2>`) OR (`<privilege_3>` AND `<privilege_4>`) to access the route.
```ts
router.get({
path: '/api/path',
security: {
authz: {
requiredPrivileges: [
{
anyRequired: [
{ allOf: ['<privilege_1>', '<privilege_2>']},
{ allOf: ['<privilege_3>', '<privilege_4>']}
],
}
],
},
},
...
}, handler);
```

**Example 5: Complex configuration with nested `anyOf`.**
Requires (`<privilege_1>` OR `<privilege_2>`) AND (`<privilege_3>` OR `<privilege_4>`) to access the route.
```ts
router.get({
path: '/api/path',
security: {
authz: {
requiredPrivileges: [
{
allRequired: [
{ anyOf: ['<privilege_1>', '<privilege_2>']},
{ anyOf: ['<privilege_3>', '<privilege_4>']}
],
}
],
},
},
...
}, handler);
```

### Versioned router security configuration examples
Different security configurations can be applied to each version when using the Versioned Router. This allows your authorization needs to evolve in lockstep with your API.

Expand Down
190 changes: 95 additions & 95 deletions oas_docs/bundle.json

Large diffs are not rendered by default.

190 changes: 95 additions & 95 deletions oas_docs/output/kibana.yaml

Large diffs are not rendered by default.

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

it('should pass validation with anyOf defined', () => {
const routeSecurity = {
authz: {
requiredPrivileges: [
{
allRequired: [
{ anyOf: ['privilege1', 'privilege2'] },
{ anyOf: ['privilege3', 'privilege4'] },
],
},
],
},
};

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

it('should pass validation with allOf defined', () => {
const routeSecurity = {
authz: {
requiredPrivileges: [
{
anyRequired: [
{ allOf: ['privilege1', 'privilege2'] },
{ allOf: ['privilege3', 'privilege4'] },
],
},
],
},
};

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

it('should fail validation when anyRequired and allRequired have the same values', () => {
const invalidRouteSecurity = {
authz: {
Expand Down Expand Up @@ -365,4 +399,74 @@ describe('RouteSecurity validation', () => {
`"[authz.requiredPrivileges]: Operator privilege requires at least one additional non-operator privilege to be defined"`
);
});

it('should fail validation when anyOf does not satisfy minSize', () => {
const invalidRouteSecurity = {
authz: {
requiredPrivileges: [{ allRequired: [{ anyOf: ['privilege1'] }] }],
},
};

expect(() => validRouteSecurity(invalidRouteSecurity)).toThrowErrorMatchingInlineSnapshot(`
"[authz.requiredPrivileges.0]: types that failed validation:
- [authz.requiredPrivileges.0.0.allRequired.0]: types that failed validation:
- [authz.requiredPrivileges.0.allRequired.0.0]: expected value of type [string] but got [Object]
- [authz.requiredPrivileges.0.allRequired.0.1.anyOf]: array size is [1], but cannot be smaller than [2]
- [authz.requiredPrivileges.0.1]: expected value of type [string] but got [Object]"
`);
});

it('should fail validation when allOf does not satisfy minSize', () => {
const invalidRouteSecurity = {
authz: {
requiredPrivileges: [{ anyRequired: [{ allOf: ['privilege1'] }, 'privilege2'] }],
},
};

expect(() => validRouteSecurity(invalidRouteSecurity)).toThrowErrorMatchingInlineSnapshot(`
"[authz.requiredPrivileges.0]: types that failed validation:
- [authz.requiredPrivileges.0.0.anyRequired.0]: types that failed validation:
- [authz.requiredPrivileges.0.anyRequired.0.0]: expected value of type [string] but got [Object]
- [authz.requiredPrivileges.0.anyRequired.0.1.allOf]: array size is [1], but cannot be smaller than [2]
- [authz.requiredPrivileges.0.1]: expected value of type [string] but got [Object]"
`);
});

it('should fail validation when anyOf has duplicated privileges', () => {
const invalidRouteSecurity = {
authz: {
requiredPrivileges: [
{
allRequired: [
{ anyOf: ['privilege1', 'privilege2'] },
{ anyOf: ['privilege3', 'privilege1'] },
],
},
],
},
};

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

it('should fail validation when allOf has duplicated privileges', () => {
const invalidRouteSecurity = {
authz: {
requiredPrivileges: [
{
anyRequired: [
{ allOf: ['privilege1', 'privilege2'] },
{ allOf: ['privilege3', 'privilege1'] },
],
},
],
},
};

expect(() => validRouteSecurity(invalidRouteSecurity)).toThrowErrorMatchingInlineSnapshot(
`"[authz.requiredPrivileges]: anyRequired privileges must contain unique values"`
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,36 @@
*/

import { schema } from '@kbn/config-schema';
import type { RouteSecurity, RouteConfigOptions } from '@kbn/core-http-server';
import type {
RouteSecurity,
RouteConfigOptions,
AllRequiredCondition,
AnyRequiredCondition,
} from '@kbn/core-http-server';
import { ReservedPrivilegesSet } from '@kbn/core-http-server';
import { unwindNestedSecurityPrivileges } from '@kbn/core-security-server';
import type { DeepPartial } from '@kbn/utility-types';

const privilegeSetSchema = schema.object(
{
anyRequired: schema.maybe(schema.arrayOf(schema.string(), { minSize: 2 })),
allRequired: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
anyRequired: schema.maybe(
schema.arrayOf(
schema.oneOf([
schema.string(),
schema.object({ allOf: schema.arrayOf(schema.string(), { minSize: 2 }) }),
]),
{ minSize: 2 }
)
),
allRequired: schema.maybe(
schema.arrayOf(
schema.oneOf([
schema.string(),
schema.object({ anyOf: schema.arrayOf(schema.string(), { minSize: 2 }) }),
]),
{ minSize: 1 }
)
),
},
{
validate: (value) => {
Expand All @@ -42,10 +64,14 @@ const requiredPrivilegesSchema = schema.arrayOf(
allRequired.push(privilege);
} else {
if (privilege.anyRequired) {
anyRequired.push(...privilege.anyRequired);
anyRequired.push(
...unwindNestedSecurityPrivileges<AnyRequiredCondition>(privilege.anyRequired)
);
}
if (privilege.allRequired) {
allRequired.push(...privilege.allRequired);
allRequired.push(
...unwindNestedSecurityPrivileges<AllRequiredCondition>(privilege.allRequired)
);
}
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"@kbn/core-http-common",
"@kbn/logging-mocks",
"@kbn/config-mocks",
"@kbn/config"
"@kbn/config",
"@kbn/core-security-server"
],
"exclude": [
"target/**/*",
Expand Down
2 changes: 2 additions & 0 deletions src/core/packages/http/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ export type {
AuthcEnabled,
Privilege,
PrivilegeSet,
AllRequiredCondition,
AnyRequiredCondition,
RouteSecurity,
RouteSecurityGetter,
InternalRouteSecurity,
Expand Down
2 changes: 2 additions & 0 deletions src/core/packages/http/server/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export type {
AuthcDisabled,
AuthcEnabled,
RouteSecurity,
AllRequiredCondition,
AnyRequiredCondition,
Privilege,
PrivilegeSet,
RouteDeprecationInfo,
Expand Down
9 changes: 6 additions & 3 deletions src/core/packages/http/server/src/router/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,21 +200,24 @@ interface DeprecateApiDeprecationType {
type: 'deprecate';
}

export type AllRequiredCondition = Array<Privilege | { anyOf: Privilege[] }>;
export type AnyRequiredCondition = Array<Privilege | { allOf: Privilege[] }>;

/**
* A set of privileges that can be used to define complex authorization requirements.
*
* - `anyRequired`: An array of privileges where at least one must be satisfied to meet the authorization requirement.
* - `allRequired`: An array of privileges where all listed privileges must be satisfied to meet the authorization requirement.
*/
export interface PrivilegeSet {
anyRequired?: Privilege[];
allRequired?: Privilege[];
anyRequired?: AnyRequiredCondition;
allRequired?: AllRequiredCondition;
}

/**
* An array representing a combination of simple privileges or complex privilege sets.
*/
type Privileges = Array<Privilege | PrivilegeSet>;
export type Privileges = Array<Privilege | PrivilegeSet>;

/**
* Describes the authorization requirements when authorization is enabled.
Expand Down
1 change: 1 addition & 0 deletions src/core/packages/security/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ export type {
export type { KibanaPrivilegesType, ElasticsearchPrivilegesType } from './src/roles';
export { isCreateRestAPIKeyParams } from './src/authentication/api_keys';
export type { CoreFipsService } from './src/fips';
export { unwindNestedSecurityPrivileges } from './src/authz';
29 changes: 29 additions & 0 deletions src/core/packages/security/server/src/authz.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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export const unwindNestedSecurityPrivileges = <
T extends Array<string | { allOf?: string[]; anyOf?: string[] }>
>(
privileges: T
): string[] =>
privileges.reduce((acc: string[], privilege) => {
if (typeof privilege === 'object') {
if (privilege.allOf?.length) {
acc.push(...privilege.allOf);
}

if (privilege?.anyOf?.length) {
acc.push(...privilege.anyOf);
}
} else if (typeof privilege === 'string') {
acc.push(privilege);
}

return acc;
}, []);
2 changes: 2 additions & 0 deletions src/core/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,4 +615,6 @@ export type {
RouteSecurityGetter,
Privilege,
PrivilegeSet,
AllRequiredCondition,
AnyRequiredCondition,
} from '@kbn/core-http-server';
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ describe('extractAuthzDescription', () => {
},
};
const description = extractAuthzDescription(routeSecurity);
expect(description).toBe(
'[Required authorization] Route required privileges: ALL of [manage_spaces].'
);
expect(description).toBe('[Required authorization] Route required privileges: manage_spaces.');
});

it('should return route authz description for privilege groups', () => {
Expand All @@ -46,9 +44,7 @@ describe('extractAuthzDescription', () => {
},
};
const description = extractAuthzDescription(routeSecurity);
expect(description).toBe(
'[Required authorization] Route required privileges: ALL of [console].'
);
expect(description).toBe('[Required authorization] Route required privileges: console.');
}
{
const routeSecurity: RouteSecurity = {
Expand All @@ -62,7 +58,7 @@ describe('extractAuthzDescription', () => {
};
const description = extractAuthzDescription(routeSecurity);
expect(description).toBe(
'[Required authorization] Route required privileges: ANY of [manage_spaces OR taskmanager].'
'[Required authorization] Route required privileges: manage_spaces OR taskmanager.'
);
}
{
Expand All @@ -78,7 +74,25 @@ describe('extractAuthzDescription', () => {
};
const description = extractAuthzDescription(routeSecurity);
expect(description).toBe(
'[Required authorization] Route required privileges: ALL of [console, filesManagement] AND ANY of [manage_spaces OR taskmanager].'
'[Required authorization] Route required privileges: (console AND filesManagement) AND (manage_spaces OR taskmanager).'
);
}
{
const routeSecurity: RouteSecurity = {
authz: {
requiredPrivileges: [
{
anyRequired: [
{ allOf: ['manage_spaces', 'taskmanager'] },
{ allOf: ['console', 'filesManagement'] },
],
},
],
},
};
const description = extractAuthzDescription(routeSecurity);
expect(description).toBe(
'[Required authorization] Route required privileges: (manage_spaces AND taskmanager) OR (console AND filesManagement).'
);
}
});
Expand Down
Loading