From 538a4ad3b350b1999a15b7403cb33cd0e2ad793f Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 1 Nov 2024 22:42:40 +1100 Subject: [PATCH] Authorized route migration for routes owned by kibana-security (#198380) ### Authz API migration for authorized routes This PR migrates `access:` tags used in route definitions to new security configuration. Please refer to the documentation for more information: [Authorization API](https://docs.elastic.dev/kibana-dev-docs/key-concepts/security-api-authorization) ### **Before migration:** Access control tags were defined in the `options` object of the route: ```ts router.get({ path: '/api/path', options: { tags: ['access:', 'access:'], }, ... }, handler); ``` ### **After migration:** Tags have been replaced with the more robust `security.authz.requiredPrivileges` field under `security`: ```ts router.get({ path: '/api/path', security: { authz: { requiredPrivileges: ['', ''], }, }, ... }, handler); ``` ### What to do next? 1. Review the changes in this PR. 2. You might need to update your tests to reflect the new security configuration: - If you have tests that rely on checking `access` tags. - If you have snapshot tests that include the route definition. - If you have FTR tests that rely on checking unauthorized error message. The error message changed to also include missing privileges. ## Any questions? If you have any questions or need help with API authorization, please reach out to the `@elastic/kibana-security` team. --------- Co-authored-by: Elena Shostak --- .../authorization/roles/get_all_by_space.test.ts | 2 +- .../routes/authorization/roles/get_all_by_space.ts | 6 ++++-- .../routes/session_management/invalidate.test.ts | 3 ++- .../server/routes/session_management/invalidate.ts | 7 ++++++- .../server/routes/user_profile/bulk_get.test.ts | 2 +- .../server/routes/user_profile/bulk_get.ts | 6 +++++- .../server/routes/api/external/copy_to_space.ts | 14 ++++++++++++-- .../api/internal/get_content_summary.test.ts | 2 +- .../routes/api/internal/get_content_summary.ts | 6 ++++-- .../common/suites/copy_to_space.ts | 3 ++- .../suites/resolve_copy_to_space_conflicts.ts | 3 ++- 11 files changed, 40 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.test.ts index 06d6d396ce022..956ced4309304 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.test.ts @@ -149,7 +149,7 @@ describe('GET all roles by space id', () => { const paramsSchema = (config.validate as any).params; - expect(config.options).toEqual({ tags: ['access:manage_spaces'] }); + expect(config.security?.authz).toEqual({ requiredPrivileges: ['manage_spaces'] }); expect(() => paramsSchema.validate({})).toThrowErrorMatchingInlineSnapshot( `"[spaceId]: expected value of type [string] but got [undefined]"` ); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts index 734f0292db116..a441ba15164c1 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all_by_space.ts @@ -24,8 +24,10 @@ export function defineGetAllRolesBySpaceRoutes({ router.get( { path: '/internal/security/roles/{spaceId}', - options: { - tags: ['access:manage_spaces'], + security: { + authz: { + requiredPrivileges: ['manage_spaces'], + }, }, validate: { params: schema.object({ spaceId: schema.string({ minLength: 1 }) }), diff --git a/x-pack/plugins/security/server/routes/session_management/invalidate.test.ts b/x-pack/plugins/security/server/routes/session_management/invalidate.test.ts index fbc14015d80c1..12c31be12dd57 100644 --- a/x-pack/plugins/security/server/routes/session_management/invalidate.test.ts +++ b/x-pack/plugins/security/server/routes/session_management/invalidate.test.ts @@ -46,9 +46,10 @@ describe('Invalidate sessions routes', () => { expect(routeConfig.options).toEqual({ access: 'public', summary: 'Invalidate user sessions', - tags: ['access:sessionManagement'], }); + expect(routeConfig.security?.authz).toEqual({ requiredPrivileges: ['sessionManagement'] }); + const bodySchema = (routeConfig.validate as any).body as ObjectType; expect(() => bodySchema.validate({})).toThrowErrorMatchingInlineSnapshot( `"[match]: expected at least one defined value but got [undefined]"` diff --git a/x-pack/plugins/security/server/routes/session_management/invalidate.ts b/x-pack/plugins/security/server/routes/session_management/invalidate.ts index a45d8f00c1ca4..bbc81c21706d9 100644 --- a/x-pack/plugins/security/server/routes/session_management/invalidate.ts +++ b/x-pack/plugins/security/server/routes/session_management/invalidate.ts @@ -37,6 +37,11 @@ export function defineInvalidateSessionsRoutes({ ), }), }, + security: { + authz: { + requiredPrivileges: ['sessionManagement'], + }, + }, options: { // The invalidate session API was introduced to address situations where the session index // could grow rapidly - when session timeouts are disabled, or with anonymous access. @@ -44,7 +49,7 @@ export function defineInvalidateSessionsRoutes({ // anonymous access. However, keeping this endpoint available internally in serverless would // be useful in situations where we need to batch-invalidate user sessions. access: buildFlavor === 'serverless' ? 'internal' : 'public', - tags: ['access:sessionManagement'], + summary: `Invalidate user sessions`, }, }, diff --git a/x-pack/plugins/security/server/routes/user_profile/bulk_get.test.ts b/x-pack/plugins/security/server/routes/user_profile/bulk_get.test.ts index f5d449bd8423d..eece6b58f8f01 100644 --- a/x-pack/plugins/security/server/routes/user_profile/bulk_get.test.ts +++ b/x-pack/plugins/security/server/routes/user_profile/bulk_get.test.ts @@ -51,7 +51,7 @@ describe('Bulk get profile routes', () => { }); it('correctly defines route.', () => { - expect(routeConfig.options).toEqual({ tags: ['access:bulkGetUserProfiles'] }); + expect(routeConfig.security?.authz).toEqual({ requiredPrivileges: ['bulkGetUserProfiles'] }); const bodySchema = (routeConfig.validate as any).body as ObjectType; expect(() => bodySchema.validate(0)).toThrowErrorMatchingInlineSnapshot( diff --git a/x-pack/plugins/security/server/routes/user_profile/bulk_get.ts b/x-pack/plugins/security/server/routes/user_profile/bulk_get.ts index 20da1d573901f..0ffe760d57d52 100644 --- a/x-pack/plugins/security/server/routes/user_profile/bulk_get.ts +++ b/x-pack/plugins/security/server/routes/user_profile/bulk_get.ts @@ -24,7 +24,11 @@ export function defineBulkGetUserProfilesRoute({ dataPath: schema.maybe(schema.string()), }), }, - options: { tags: ['access:bulkGetUserProfiles'] }, + security: { + authz: { + requiredPrivileges: ['bulkGetUserProfiles'], + }, + }, }, createLicensedRouteHandler(async (context, request, response) => { const userProfileServiceInternal = getUserProfileService(); diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts index b0758f5645cc1..509de14e2928b 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts @@ -37,9 +37,14 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { router.post( { path: '/api/spaces/_copy_saved_objects', + security: { + authz: { + requiredPrivileges: ['copySavedObjectsToSpaces'], + }, + }, options: { access: isServerless ? 'internal' : 'public', - tags: ['access:copySavedObjectsToSpaces', 'oas-tag:spaces'], + tags: ['oas-tag:spaces'], summary: `Copy saved objects between spaces`, description: 'It also allows you to automatically copy related objects, so when you copy a dashboard, this can automatically copy over the associated visualizations, data views, and saved searches, as required. You can request to overwrite any objects that already exist in the target space if they share an identifier or you can use the resolve copy saved objects conflicts API to do this on a per-object basis.', @@ -188,9 +193,14 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { router.post( { path: '/api/spaces/_resolve_copy_saved_objects_errors', + security: { + authz: { + requiredPrivileges: ['copySavedObjectsToSpaces'], + }, + }, options: { access: isServerless ? 'internal' : 'public', - tags: ['access:copySavedObjectsToSpaces'], + summary: `Resolve conflicts copying saved objects`, description: 'Overwrite saved objects that are returned as errors from the copy saved objects to space API.', diff --git a/x-pack/plugins/spaces/server/routes/api/internal/get_content_summary.test.ts b/x-pack/plugins/spaces/server/routes/api/internal/get_content_summary.test.ts index d6bc68244f750..a94d51fafc05d 100644 --- a/x-pack/plugins/spaces/server/routes/api/internal/get_content_summary.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/internal/get_content_summary.test.ts @@ -120,7 +120,7 @@ describe('GET /internal/spaces/{spaceId}/content_summary', () => { const paramsSchema = (config.validate as any).params; - expect(config.options).toEqual({ tags: ['access:manage_spaces'] }); + expect(config.security?.authz).toEqual({ requiredPrivileges: ['manage_spaces'] }); expect(() => paramsSchema.validate({})).toThrowErrorMatchingInlineSnapshot( `"[spaceId]: expected value of type [string] but got [undefined]"` ); diff --git a/x-pack/plugins/spaces/server/routes/api/internal/get_content_summary.ts b/x-pack/plugins/spaces/server/routes/api/internal/get_content_summary.ts index 848449bfd47b3..6c80a645f0c62 100644 --- a/x-pack/plugins/spaces/server/routes/api/internal/get_content_summary.ts +++ b/x-pack/plugins/spaces/server/routes/api/internal/get_content_summary.ts @@ -38,8 +38,10 @@ export function initGetSpaceContentSummaryApi(deps: InternalRouteDeps) { router.get( { path: '/internal/spaces/{spaceId}/content_summary', - options: { - tags: ['access:manage_spaces'], + security: { + authz: { + requiredPrivileges: ['manage_spaces'], + }, }, validate: { params: schema.object({ diff --git a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts index 4cb2506977123..f297e4092ba6b 100644 --- a/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/common/suites/copy_to_space.ts @@ -154,7 +154,8 @@ export function copyToSpaceTestSuiteFactory(context: FtrProviderContext) { expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: 'Forbidden', + message: + 'API [POST /api/spaces/_copy_saved_objects] is unauthorized for user, this action is granted by the Kibana privileges [copySavedObjectsToSpaces]', }); }; diff --git a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts index e07d56a95ba24..845d41d1431b9 100644 --- a/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts +++ b/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts @@ -252,7 +252,8 @@ export function resolveCopyToSpaceConflictsSuite(context: FtrProviderContext) { expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: 'Forbidden', + message: + 'API [POST /api/spaces/_resolve_copy_saved_objects_errors] is unauthorized for user, this action is granted by the Kibana privileges [copySavedObjectsToSpaces]', }); };