[Authz] Migrated routes with access tags to security config (#209756)

## Summary

This PR migrates the last routes with `access:<privilege>` 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:<privilege_1>', 'access:<privilege_2>'],
  },
  ...
}, 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: ['<privilege_1>', '<privilege_2>'],
    },
  },
  ...
}, handler);
```

### Checklist

- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Elena Shostak 2025-02-11 21:36:38 +07:00 committed by GitHub
parent 9bdee77409
commit ad0e1d9d9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 67 additions and 40 deletions

View file

@ -69,8 +69,10 @@ export class FeatureControlsPluginExample
{
path: '/internal/my_plugin/sensitive_action',
validate: false,
options: {
tags: ['access:my_closed_example_api'],
security: {
authz: {
requiredPrivileges: ['my_closed_example_api'],
},
},
},
async (context, request, response) => {

View file

@ -40,7 +40,11 @@ export class UserProfilesPlugin implements Plugin<void, void, SetupDeps, StartDe
/**
* Important: You must restrict access to this endpoint using access `tags`.
*/
options: { tags: ['access:suggestUserProfiles'] },
security: {
authz: {
requiredPrivileges: ['suggestUserProfiles'],
},
},
},
async (context, request, response) => {
const [, pluginDeps] = await core.getStartServices();

View file

@ -268,9 +268,11 @@ export const createEntityRoute = (router: Router): void => {
.post({
access: 'public',
path: '/api/my/data/{id}',
options: {
tags: ['access:securitySolution'],
},
security: {
authz: {
requiredPrivileges: ['securitySolution']
}
}
})
.addVersion(
{

View file

@ -93,8 +93,10 @@ describe('CoreApp', () => {
expect(routerMock.versioned.put).toHaveBeenCalledWith({
path: '/internal/core/_settings',
access: 'internal',
options: {
tags: ['access:updateDynamicConfig'],
security: {
authz: {
requiredPrivileges: ['updateDynamicConfig'],
},
},
});
});

View file

@ -279,8 +279,10 @@ export class CoreAppsService {
.put({
path: '/internal/core/_settings',
access: 'internal',
options: {
tags: ['access:updateDynamicConfig'],
security: {
authz: {
requiredPrivileges: ['updateDynamicConfig'],
},
},
})
.addVersion(

View file

@ -87,13 +87,9 @@ export const registerAutocompleteEntitiesRoute = (deps: RouteDependencies) => {
deps.router.get(
{
path: '/api/console/autocomplete_entities',
options: {
tags: ['access:console'],
},
security: {
authz: {
enabled: false,
reason: 'Relies on es client for authorization',
requiredPrivileges: ['console'],
},
},
validate: autoCompleteEntitiesValidationConfig,

View file

@ -46,7 +46,7 @@ export function registerTelemetryUsageStatsRoutes(
const security = getSecurity();
// We need to check useRbacForRequest to figure out if ES has security enabled before making the privileges check
if (security && unencrypted && security.authz.mode.useRbacForRequest(req)) {
// Normally we would use `options: { tags: ['access:decryptedTelemetry'] }` in the route definition to check authorization for an
// Normally we would use `security: { authz: { requiredPrivileges: ['decryptedTelemetry'] } } }` in the route definition to check authorization for an
// API action, however, we want to check this conditionally based on the `unencrypted` parameter. In this case we need to use the
// security API directly to check privileges for this action. Note that the 'decryptedTelemetry' API privilege string is only
// granted to users that have "Global All" or "Global Read" privileges in Kibana.

View file

@ -122,8 +122,10 @@ export function routes(coreSetup: CoreSetup<StartDeps, unknown>, logger: Logger)
.post({
path: '/internal/data_visualizer/inference/{inferenceId}',
access: 'internal',
options: {
tags: ['access:fileUpload:analyzeFile'],
security: {
authz: {
requiredPrivileges: ['fileUpload:analyzeFile'],
},
},
})
.addVersion(

View file

@ -54,25 +54,27 @@ export interface FeatureKibanaPrivileges {
*
* @example
* ```ts
* // Configure your routes with a tag starting with the 'access:' prefix
* // Configure your routes with requiredPrivileges
* server.route({
* path: '/api/my-route',
* method: 'GET',
* handler: () => { ...},
* options: {
* tags: ['access:my_feature-admin']
* }
* security: {
* authz: {
* requiredPrivileges: ['my_feature_admin']
* },
* },
* });
*
* Then, specify the tags here (without the 'access:' prefix) which should be secured:
* Then, specify requiredPrivileges which should be secured:
*
* {
* api: ['my_feature-admin']
* api: ['my_feature_admin']
* }
* ```
*
* NOTE: It is important to name your tags in a way that will not collide with other platform/plugins/shared/features.
* A generic tag name like "access:read" could be used elsewhere, and access to that API endpoint would also
* NOTE: It is important to name your privileges in a way that will not collide with other platform/plugins/shared/features.
* A generic tag name like "read" could be used elsewhere, and access to that API endpoint would also
* extend to any routes you have also tagged with that name.
*/
api?: readonly string[];

View file

@ -119,7 +119,11 @@ export function backgroundTaskUtilizationRoute(
},
},
// Uncomment when we determine that we can restrict API usage to Global admins based on telemetry
// options: { tags: ['access:taskManager'] },
// security: {
// authz: {
// requiredPrivileges: ['taskManager'],
// },
// },
validate: false,
options: {
access: 'public', // access must be public to allow "system" users, like metrics collectors, to access these routes

View file

@ -148,7 +148,11 @@ export function healthRoute(params: HealthRouteParams): {
},
},
// Uncomment when we determine that we can restrict API usage to Global admins based on telemetry
// options: { tags: ['access:taskManager'] },
// security: {
// authz: {
// requiredPrivileges: ['taskManager'],
// },
// },
validate: false,
options: {
access: 'public',

View file

@ -62,7 +62,11 @@ export function metricsRoute(params: MetricsRouteParams) {
tags: ['security:acceptJWT'],
},
// Uncomment when we determine that we can restrict API usage to Global admins based on telemetry
// options: { tags: ['access:taskManager'] },
// security: {
// authz: {
// requiredPrivileges: ['taskManager'],
// },
// },
validate: {
query: QuerySchema,
},

View file

@ -9,8 +9,10 @@ import { extractEntityIndexPatternsFromDefinitions } from './extract_entity_inde
export const getEntityDefinitionSourceIndexPatternsByType = createInventoryServerRoute({
endpoint: 'GET /internal/inventory/entity/definitions/sources',
options: {
tags: ['access:inventory'],
security: {
authz: {
requiredPrivileges: ['inventory'],
},
},
async handler({ context, request, plugins }) {
const [_coreContext, entityManagerStart] = await Promise.all([

View file

@ -44,15 +44,12 @@ export class KibanaFramework {
config: InfraRouteConfig<Params, Query, Body, Method>,
handler: RequestHandler<Params, Query, Body, RequestHandlerContext>
) {
const defaultOptions = {
tags: ['access:infra'],
};
const routeConfig = {
path: config.path,
validate: config.validate,
// Currently we have no use of custom options beyond tags, this can be extended
// beyond defaultOptions if it's needed.
options: defaultOptions,
security: {
authz: { requiredPrivileges: ['infra'] },
},
};
switch (config.method) {
case 'get':

View file

@ -124,11 +124,13 @@ export const bulkDeleteRulesRoute = (
access: 'public',
path: DETECTION_ENGINE_RULES_BULK_DELETE,
options: {
tags: ['access:securitySolution'],
timeout: {
idleSocket: RULE_MANAGEMENT_BULK_ACTION_SOCKET_TIMEOUT_MS,
},
},
security: {
authz: { requiredPrivileges: ['securitySolution'] },
},
};
router.versioned.delete(routeConfig).addVersion(
{

View file

@ -25,8 +25,10 @@ export const entityStoreInternalPrivilegesRoute = (
.get({
access: 'internal',
path: ENTITY_STORE_INTERNAL_PRIVILEGES_URL,
options: {
tags: ['access:securitySolution', `access:${APP_ID}-entity-analytics`],
security: {
authz: {
requiredPrivileges: ['securitySolution', `${APP_ID}-entity-analytics`],
},
},
})
.addVersion(