Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
194 changes: 97 additions & 97 deletions oas_docs/bundle.json

Large diffs are not rendered by default.

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

Large diffs are not rendered by default.

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

Large diffs are not rendered by default.

194 changes: 97 additions & 97 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,35 @@
*/

import { schema } from '@kbn/config-schema';
import type { RouteSecurity, RouteConfigOptions } from '@kbn/core-http-server';
import { ReservedPrivilegesSet } from '@kbn/core-http-server';
import type {
RouteSecurity,
RouteConfigOptions,
AllRequiredCondition,
AnyRequiredCondition,
} from '@kbn/core-http-server';
import { ReservedPrivilegesSet, unwindNestedSecurityPrivileges } from '@kbn/core-http-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 +63,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
3 changes: 3 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 All @@ -134,6 +136,7 @@ export {
isFullValidatorContainer,
isKibanaResponse,
ReservedPrivilegesSet,
unwindNestedSecurityPrivileges,
} from './src/router';

export type { ICspConfig } from './src/csp';
Expand Down
9 changes: 8 additions & 1 deletion 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 Expand Up @@ -101,4 +103,9 @@ export type {
LifecycleResponseFactory,
} from './response_factory';
export type { RawRequest, FakeRawRequest } from './raw_request';
export { getRequestValidation, getResponseValidation, isFullValidatorContainer } from './utils';
export {
getRequestValidation,
getResponseValidation,
isFullValidatorContainer,
unwindNestedSecurityPrivileges,
} from './utils';
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 @@ -202,21 +202,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
21 changes: 21 additions & 0 deletions src/core/packages/http/server/src/router/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,24 @@ export function getResponseValidation(
if (typeof value === 'function') value = value();
return isFullValidatorContainer(value) ? value.response : undefined;
}

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;
}, []);
4 changes: 4 additions & 0 deletions src/core/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,4 +615,8 @@ export type {
RouteSecurityGetter,
Privilege,
PrivilegeSet,
AllRequiredCondition,
AnyRequiredCondition,
} from '@kbn/core-http-server';

export { unwindNestedSecurityPrivileges } 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