Skip to content

Commit

Permalink
[Authz] Operator privileges
Browse files Browse the repository at this point in the history
  • Loading branch information
elena-shostak committed Oct 16, 2024
1 parent c53b2a8 commit 9aaa7e9
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 62 deletions.
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;

/**
* Indicated whether user is an operator.
*/
operator?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
* 2.0.
*/

import type { KibanaRequest } from '@kbn/core/server';
import type { AuthenticatedUser } from '@kbn/security-plugin-types-common';

import type { Actions } from './actions';
import type { CheckPrivilegesWithRequest } from './check_privileges';
import type { CheckPrivilegesDynamicallyWithRequest } from './check_privileges_dynamically';
Expand All @@ -25,4 +28,5 @@ export interface AuthorizationServiceSetup {
checkPrivilegesDynamicallyWithRequest: CheckPrivilegesDynamicallyWithRequest;
checkSavedObjectsPrivilegesWithRequest: CheckSavedObjectsPrivilegesWithRequest;
mode: AuthorizationMode;
getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null;
}
2 changes: 2 additions & 0 deletions x-pack/plugins/security/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,5 @@ export type {
UserProfileLabels,
UserProfileUserInfoWithSecurity,
} from '@kbn/security-plugin-types-common';

export { ReservedPrivilegesSet } from './types';
5 changes: 5 additions & 0 deletions x-pack/plugins/security/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@ export enum LogoutReason {
export interface SecurityCheckupState {
displayAlert: boolean;
}

export enum ReservedPrivilegesSet {
Operator = 'operator',
Superuser = 'superuser',
}
161 changes: 109 additions & 52 deletions x-pack/plugins/security/server/authorization/api_authorization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {

import { initAPIAuthorization } from './api_authorization';
import { authorizationMock } from './index.mock';
import { ReservedPrivilegesSet } from '../../common/types';

describe('initAPIAuthorization', () => {
test(`protected route when "mode.useRbacForRequest()" returns false continues`, async () => {
Expand Down Expand Up @@ -145,12 +146,17 @@ describe('initAPIAuthorization', () => {
{
security,
kibanaPrivilegesResponse,
kibanaCurrentUserResponse,
kibanaPrivilegesRequestActions,
asserts,
}: {
security?: RouteSecurity;
kibanaPrivilegesResponse?: Array<{ privilege: string; authorized: boolean }>;
kibanaPrivilegesResponse?: {
privileges: { kibana: Array<{ privilege: string; authorized: boolean }> };
hasAllRequested?: boolean;
};
kibanaPrivilegesRequestActions?: string[];
kibanaCurrentUserResponse?: { operator: boolean };
asserts: {
forbidden?: boolean;
authzResult?: Record<string, boolean>;
Expand Down Expand Up @@ -180,11 +186,8 @@ describe('initAPIAuthorization', () => {
const mockResponse = httpServerMock.createResponseFactory();
const mockPostAuthToolkit = httpServiceMock.createOnPostAuthToolkit();

const mockCheckPrivileges = jest.fn().mockReturnValue({
privileges: {
kibana: kibanaPrivilegesResponse,
},
});
const mockCheckPrivileges = jest.fn().mockReturnValue(kibanaPrivilegesResponse);
mockAuthz.getCurrentUser.mockReturnValue(kibanaCurrentUserResponse);
mockAuthz.mode.useRbacForRequest.mockReturnValue(true);
mockAuthz.checkPrivilegesDynamicallyWithRequest.mockImplementation((request) => {
// hapi conceals the actual "request" from us, so we make sure that the headers are passed to
Expand All @@ -207,11 +210,13 @@ describe('initAPIAuthorization', () => {
return;
}

expect(mockCheckPrivileges).toHaveBeenCalledWith({
kibana: kibanaPrivilegesRequestActions!.map((action: string) =>
mockAuthz.actions.api.get(action)
),
});
if (kibanaPrivilegesRequestActions) {
expect(mockCheckPrivileges).toHaveBeenCalledWith({
kibana: kibanaPrivilegesRequestActions!.map((action: string) =>
mockAuthz.actions.api.get(action)
),
});
}

if (asserts.forbidden) {
expect(mockResponse.forbidden).toHaveBeenCalled();
Expand Down Expand Up @@ -239,11 +244,15 @@ describe('initAPIAuthorization', () => {
],
},
},
kibanaPrivilegesResponse: [
{ privilege: 'api:privilege1', authorized: true },
{ privilege: 'api:privilege2', authorized: true },
{ privilege: 'api:privilege3', authorized: false },
],
kibanaPrivilegesResponse: {
privileges: {
kibana: [
{ privilege: 'api:privilege1', authorized: true },
{ privilege: 'api:privilege2', authorized: true },
{ privilege: 'api:privilege3', authorized: false },
],
},
},
kibanaPrivilegesRequestActions: ['privilege1', 'privilege2', 'privilege3'],
asserts: {
authzResult: {
Expand All @@ -267,10 +276,14 @@ describe('initAPIAuthorization', () => {
],
},
},
kibanaPrivilegesResponse: [
{ privilege: 'api:privilege1', authorized: true },
{ privilege: 'api:privilege2', authorized: true },
],
kibanaPrivilegesResponse: {
privileges: {
kibana: [
{ privilege: 'api:privilege1', authorized: true },
{ privilege: 'api:privilege2', authorized: true },
],
},
},
kibanaPrivilegesRequestActions: ['privilege1', 'privilege2'],
asserts: {
authzResult: {
Expand All @@ -293,11 +306,15 @@ describe('initAPIAuthorization', () => {
],
},
},
kibanaPrivilegesResponse: [
{ privilege: 'api:privilege1', authorized: false },
{ privilege: 'api:privilege2', authorized: true },
{ privilege: 'api:privilege3', authorized: false },
],
kibanaPrivilegesResponse: {
privileges: {
kibana: [
{ privilege: 'api:privilege1', authorized: false },
{ privilege: 'api:privilege2', authorized: true },
{ privilege: 'api:privilege3', authorized: false },
],
},
},
kibanaPrivilegesRequestActions: ['privilege1', 'privilege2', 'privilege3'],
asserts: {
authzResult: {
Expand All @@ -317,10 +334,14 @@ describe('initAPIAuthorization', () => {
requiredPrivileges: ['privilege1', 'privilege2'],
},
},
kibanaPrivilegesResponse: [
{ privilege: 'api:privilege1', authorized: true },
{ privilege: 'api:privilege2', authorized: true },
],
kibanaPrivilegesResponse: {
privileges: {
kibana: [
{ privilege: 'api:privilege1', authorized: true },
{ privilege: 'api:privilege2', authorized: true },
],
},
},
kibanaPrivilegesRequestActions: ['privilege1', 'privilege2'],
asserts: {
authzResult: {
Expand All @@ -332,24 +353,52 @@ describe('initAPIAuthorization', () => {
);

testSecurityConfig(
`protected route returns forbidden if user has allRequired AND NONE of anyRequired privileges requested`,
`protected route returns "authzResult" if user has operator privileges requested and user is operator`,
{
security: {
authz: {
requiredPrivileges: [
{
allRequired: ['privilege1'],
anyRequired: ['privilege2', 'privilege3'],
},
],
requiredPrivileges: [ReservedPrivilegesSet.Operator],
},
},
kibanaPrivilegesResponse: [
{ privilege: 'api:privilege1', authorized: true },
{ privilege: 'api:privilege2', authorized: false },
{ privilege: 'api:privilege3', authorized: false },
],
kibanaPrivilegesRequestActions: ['privilege1', 'privilege2', 'privilege3'],
kibanaCurrentUserResponse: { operator: true },
asserts: {
authzResult: {
operator: true,
},
},
}
);

testSecurityConfig(
`protected route returns "authzResult" if user has anyRequired privileges requested and user is operator`,
{
security: {
authz: {
requiredPrivileges: [{ anyRequired: [ReservedPrivilegesSet.Operator, 'privilege1'] }],
},
},
kibanaCurrentUserResponse: { operator: true },
kibanaPrivilegesResponse: {
privileges: { kibana: [{ privilege: 'api:privilege1', authorized: false }] },
},
asserts: {
authzResult: {
[ReservedPrivilegesSet.Operator]: true,
privilege1: false,
},
},
}
);

testSecurityConfig(
`protected route returns forbidden if user has operator privileges requested and user is not operator`,
{
security: {
authz: {
requiredPrivileges: [ReservedPrivilegesSet.Operator],
},
},
kibanaCurrentUserResponse: { operator: false },
asserts: {
forbidden: true,
},
Expand All @@ -369,12 +418,16 @@ describe('initAPIAuthorization', () => {
],
},
},
kibanaPrivilegesResponse: [
{ privilege: 'api:privilege1', authorized: true },
{ privilege: 'api:privilege2', authorized: false },
{ privilege: 'api:privilege3', authorized: false },
{ privilege: 'api:privilege4', authorized: true },
],
kibanaPrivilegesResponse: {
privileges: {
kibana: [
{ privilege: 'api:privilege1', authorized: true },
{ privilege: 'api:privilege2', authorized: false },
{ privilege: 'api:privilege3', authorized: false },
{ privilege: 'api:privilege4', authorized: true },
],
},
},
kibanaPrivilegesRequestActions: ['privilege1', 'privilege2', 'privilege3', 'privilege4'],
asserts: {
forbidden: true,
Expand All @@ -390,10 +443,14 @@ describe('initAPIAuthorization', () => {
requiredPrivileges: ['privilege1', 'privilege2'],
},
},
kibanaPrivilegesResponse: [
{ privilege: 'api:privilege1', authorized: true },
{ privilege: 'api:privilege2', authorized: false },
],
kibanaPrivilegesResponse: {
privileges: {
kibana: [
{ privilege: 'api:privilege1', authorized: true },
{ privilege: 'api:privilege2', authorized: false },
],
},
},
kibanaPrivilegesRequestActions: ['privilege1', 'privilege2'],
asserts: {
forbidden: true,
Expand Down
63 changes: 53 additions & 10 deletions x-pack/plugins/security/server/authorization/api_authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,25 @@ import type { AuthorizationServiceSetup } from '@kbn/security-plugin-types-serve
import type { RecursiveReadonly } from '@kbn/utility-types';

import { API_OPERATION_PREFIX } from '../../common/constants';
import { ReservedPrivilegesSet } from '../../common/types';

const isAuthzDisabled = (authz?: RecursiveReadonly<RouteAuthz>): authz is AuthzDisabled => {
return (authz as AuthzDisabled)?.enabled === false;
};

const isReservedPrivilegeSet = (privilege: string): privilege is ReservedPrivilegesSet => {
return Object.values(ReservedPrivilegesSet).includes(privilege as ReservedPrivilegesSet);
};

export function initAPIAuthorization(
http: HttpServiceSetup,
{ actions, checkPrivilegesDynamicallyWithRequest, mode }: AuthorizationServiceSetup,
{
actions,
checkPrivilegesDynamicallyWithRequest,
checkPrivilegesWithRequest,
mode,
getCurrentUser,
}: AuthorizationServiceSetup,
logger: Logger
) {
http.registerOnPostAuth(async (request, response, toolkit) => {
Expand All @@ -47,26 +58,58 @@ export function initAPIAuthorization(

const authz = security.authz as AuthzEnabled;

const requestedPrivileges = authz.requiredPrivileges.flatMap((privilegeEntry) => {
if (typeof privilegeEntry === 'object') {
return [...(privilegeEntry.allRequired ?? []), ...(privilegeEntry.anyRequired ?? [])];
const { requestedPrivileges, requestedReservedPrivileges } = authz.requiredPrivileges.reduce(
(acc, privilegeEntry) => {
const privileges =
typeof privilegeEntry === 'object'
? [...(privilegeEntry.allRequired ?? []), ...(privilegeEntry.anyRequired ?? [])]
: [privilegeEntry];

for (const privilege of privileges) {
if (isReservedPrivilegeSet(privilege)) {
acc.requestedReservedPrivileges.push(privilege);
} else {
acc.requestedPrivileges.push(privilege);
}
}

return acc;
},
{
requestedPrivileges: [] as string[],
requestedReservedPrivileges: [] as string[],
}
);

return privilegeEntry;
});

const apiActions = requestedPrivileges.map((permission) => actions.api.get(permission));
const checkPrivileges = checkPrivilegesDynamicallyWithRequest(request);
const checkPrivilegesResponse = await checkPrivileges({ kibana: apiActions });
const checkPrivilegesIfNotEmpty = async () => {
if (requestedPrivileges.length === 0) {
return;
}

const apiActions = requestedPrivileges.map((permission) => actions.api.get(permission));

return await checkPrivileges({ kibana: apiActions });
};

const privilegeToApiOperation = (privilege: string) =>
privilege.replace(API_OPERATION_PREFIX, '');

const checkPrivilegesResponse = await checkPrivilegesIfNotEmpty();
const kibanaPrivileges: Record<string, boolean> = {};

for (const kbPrivilege of checkPrivilegesResponse.privileges.kibana) {
for (const kbPrivilege of checkPrivilegesResponse?.privileges?.kibana ?? []) {
kibanaPrivileges[privilegeToApiOperation(kbPrivilege.privilege)] = kbPrivilege.authorized;
}

for (const reservedPrivilege of requestedReservedPrivileges) {
if (reservedPrivilege === ReservedPrivilegesSet.Operator) {
const currentUser = getCurrentUser(request);

kibanaPrivileges[ReservedPrivilegesSet.Operator] = currentUser?.operator ?? false;
}
}

const hasRequestedPrivilege = (kbPrivilege: Privilege | PrivilegeSet) => {
if (typeof kbPrivilege === 'object') {
const allRequired = kbPrivilege.allRequired ?? [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export class AuthorizationService {
checkPrivilegesWithRequest,
getSpacesService
),
getCurrentUser,
};

capabilities.registerSwitcher(
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/security/server/authorization/index.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ export const authorizationMock = {
privileges: { get: jest.fn() },
registerPrivilegesWithCluster: jest.fn(),
disableUnauthorizedCapabilities: jest.fn(),
getCurrentUser: jest.fn(),
}),
};

0 comments on commit 9aaa7e9

Please sign in to comment.