export function defineRoutes()

in server/routes/index.ts [26:853]


export function defineRoutes(router: IRouter) {
  const internalUserSchema = schema.object({
    description: schema.maybe(schema.string()),
    password: schema.maybe(schema.string()),
    backend_roles: schema.arrayOf(schema.string(), { defaultValue: [] }),
    attributes: schema.any({ defaultValue: {} }),
  });

  const actionGroupSchema = schema.object({
    description: schema.maybe(schema.string()),
    allowed_actions: schema.arrayOf(schema.string()),
    // type field is not supported in legacy implementation, comment it out for now.
    // type: schema.oneOf([
    //   schema.literal('cluster'),
    //   schema.literal('index'),
    //   schema.literal('opensearch_dashboards'),
    // ]),
  });

  const roleMappingSchema = schema.object({
    description: schema.maybe(schema.string()),
    backend_roles: schema.arrayOf(schema.string(), { defaultValue: [] }),
    hosts: schema.arrayOf(schema.string(), { defaultValue: [] }),
    users: schema.arrayOf(schema.string(), { defaultValue: [] }),
  });

  const roleSchema = schema.object({
    description: schema.maybe(schema.string()),
    cluster_permissions: schema.arrayOf(schema.string(), { defaultValue: [] }),
    tenant_permissions: schema.arrayOf(schema.any(), { defaultValue: [] }),
    index_permissions: schema.arrayOf(schema.any(), { defaultValue: [] }),
  });

  const tenantSchema = schema.object({
    description: schema.string(),
  });

  const accountSchema = schema.object({
    password: schema.string(),
    current_password: schema.string(),
  });

  const schemaMap: any = {
    internalusers: internalUserSchema,
    actiongroups: actionGroupSchema,
    rolesmapping: roleMappingSchema,
    roles: roleSchema,
    tenants: tenantSchema,
    account: accountSchema,
  };

  function validateRequestBody(resourceName: string, requestBody: any): any {
    const inputSchema = schemaMap[resourceName];
    if (!inputSchema) {
      throw new Error(`Unknown resource ${resourceName}`);
    }
    inputSchema.validate(requestBody); // throws error if validation fail
  }

  function validateEntityId(resourceName: string) {
    if (!isValidResourceName(resourceName)) {
      return 'Invalid entity name or id.';
    }
  }

  /**
   * Lists resources by resource name.
   *
   * The response format is:
   * {
   *   "total": <total_entity_count>,
   *   "data": {
   *     "entity_id_1": { <entity_structure> },
   *     "entity_id_2": { <entity_structure> },
   *     ...
   *   }
   * }
   *
   * e.g. when listing internal users, response may look like:
   * {
   *   "total": 2,
   *   "data": {
   *     "api_test_user2": {
   *       "hash": "",
   *       "reserved": false,
   *       "hidden": false,
   *       "backend_roles": [],
   *       "attributes": {},
   *       "description": "",
   *       "static": false
   *     },
   *     "api_test_user1": {
   *       "hash": "",
   *       "reserved": false,
   *       "hidden": false,
   *       "backend_roles": [],
   *       "attributes": {},
   *       "static": false
   *     }
   * }
   *
   * when listing action groups, response will look like:
   * {
   *   "total": 2,
   *   "data": {
   *     "read": {
   *       "reserved": true,
   *       "hidden": false,
   *       "allowed_actions": ["indices:data/read*", "indices:admin/mappings/fields/get*"],
   *       "type": "index",
   *       "description": "Allow all read operations",
   *       "static": false
   *     },
   *     "cluster_all": {
   *       "reserved": true,
   *       "hidden": false,
   *       "allowed_actions": ["cluster:*"],
   *       "type": "cluster",
   *       "description": "Allow everything on cluster level",
   *       "static": false
   *     }
   * }
   *
   * role:
   * {
   *   "total": 2,
   *   "data": {
   *     "opensearch_dashboards_user": {
   *       "reserved": true,
   *       "hidden": false,
   *       "description": "Provide the minimum permissions for a opensearch_dashboards user",
   *       "cluster_permissions": ["cluster_composite_ops"],
   *       "index_permissions": [{
   *         "index_patterns": [".opensearch_dashboards", ".opensearch_dashboards-6", ".opensearch_dashboards_*"],
   *         "fls": [],
   *         "masked_fields": [],
   *         "allowed_actions": ["read", "delete", "manage", "index"]
   *       }, {
   *         "index_patterns": [".tasks", ".management-beats"],
   *         "fls": [],
   *         "masked_fields": [],
   *         "allowed_actions": ["indices_all"]
   *       }],
   *       "tenant_permissions": [],
   *       "static": false
   *     },
   *     "all_access": {
   *       "reserved": true,
   *       "hidden": false,
   *       "description": "Allow full access to all indices and all cluster APIs",
   *       "cluster_permissions": ["*"],
   *       "index_permissions": [{
   *         "index_patterns": ["*"],
   *         "fls": [],
   *         "masked_fields": [],
   *         "allowed_actions": ["*"]
   *       }],
   *       "tenant_permissions": [{
   *         "tenant_patterns": ["*"],
   *         "allowed_actions": ["opensearch_dashboards_all_write"]
   *       }],
   *       "static": false
   *     }
   *   }
   * }
   *
   * rolesmapping:
   * {
   *   "total": 2,
   *   "data": {
   *     "security_manager": {
   *       "reserved": false,
   *       "hidden": false,
   *       "backend_roles": [],
   *       "hosts": [],
   *       "users": ["zengyan", "admin"],
   *       "and_backend_roles": []
   *     },
   *     "all_access": {
   *       "reserved": false,
   *       "hidden": false,
   *       "backend_roles": [],
   *       "hosts": [],
   *       "users": ["zengyan", "admin", "indextest"],
   *       "and_backend_roles": []
   *     }
   *   }
   * }
   *
   * tenants:
   * {
   *   "total": 2,
   *   "data": {
   *     "global_tenant": {
   *       "reserved": true,
   *       "hidden": false,
   *       "description": "Global tenant",
   *       "static": false
   *     },
   *     "test tenant": {
   *       "reserved": false,
   *       "hidden": false,
   *       "description": "tenant description",
   *       "static": false
   *     }
   *   }
   * }
   */
  router.get(
    {
      path: `${API_PREFIX}/${CONFIGURATION_API_PREFIX}/{resourceName}`,
      validate: {
        params: schema.object({
          resourceName: schema.string(),
        }),
      },
    },
    async (
      context,
      request,
      response
    ): Promise<IOpenSearchDashboardsResponse<any | ResponseError>> => {
      const client = context.security_plugin.esClient.asScoped(request);
      let esResp;
      try {
        esResp = await client.callAsCurrentUser('opensearch_security.listResource', {
          resourceName: request.params.resourceName,
        });
        return response.ok({
          body: {
            total: Object.keys(esResp).length,
            data: esResp,
          },
        });
      } catch (error) {
        console.log(JSON.stringify(error));
        return errorResponse(response, error);
      }
    }
  );

  /**
   * Gets entity by id.
   *
   * the response format differs from different resource types. e.g.
   *
   * for internal user, response will look like:
   * {
   *   "hash": "",
   *   "reserved": false,
   *   "hidden": false,
   *   "backend_roles": [],
   *   "attributes": {},
   *   "static": false
   * }
   *
   * for role, response will look like:
   * {
   *   "reserved": true,
   *   "hidden": false,
   *   "description": "Allow full access to all indices and all cluster APIs",
   *   "cluster_permissions": ["*"],
   *   "index_permissions": [{
   *     "index_patterns": ["*"],
   *     "fls": [],
   *     "masked_fields": [],
   *     "allowed_actions": ["*"]
   *   }],
   *   "tenant_permissions": [{
   *     "tenant_patterns": ["*"],
   *     "allowed_actions": ["opensearch_dashboards_all_write"]
   *   }],
   *   "static": false
   * }
   *
   * for roles mapping, response will look like:
   * {
   *   "reserved": true,
   *   "hidden": false,
   *   "description": "Allow full access to all indices and all cluster APIs",
   *   "cluster_permissions": ["*"],
   *   "index_permissions": [{
   *     "index_patterns": ["*"],
   *     "fls": [],
   *     "masked_fields": [],
   *     "allowed_actions": ["*"]
   *   }],
   *   "tenant_permissions": [{
   *     "tenant_patterns": ["*"],
   *     "allowed_actions": ["opensearch_dashboards_all_write"]
   *   }],
   *   "static": false
   * }
   *
   * for action groups, response will look like:
   * {
   *   "reserved": true,
   *   "hidden": false,
   *   "allowed_actions": ["indices:data/read*", "indices:admin/mappings/fields/get*"],
   *   "type": "index",
   *   "description": "Allow all read operations",
   *   "static": false
   * }
   *
   * for tenant, response will look like:
   * {
   *   "reserved": true,
   *   "hidden": false,
   *   "description": "Global tenant",
   *   "static": false
   * },
   */
  router.get(
    {
      path: `${API_PREFIX}/${CONFIGURATION_API_PREFIX}/{resourceName}/{id}`,
      validate: {
        params: schema.object({
          resourceName: schema.string(),
          id: schema.string(),
        }),
      },
    },
    async (
      context,
      request,
      response
    ): Promise<IOpenSearchDashboardsResponse<any | ResponseError>> => {
      const client = context.security_plugin.esClient.asScoped(request);
      let esResp;
      try {
        esResp = await client.callAsCurrentUser('opensearch_security.getResource', {
          resourceName: request.params.resourceName,
          id: request.params.id,
        });
        return response.ok({ body: esResp[request.params.id] });
      } catch (error) {
        return errorResponse(response, error);
      }
    }
  );

  /**
   * Deletes an entity by id.
   */
  router.delete(
    {
      path: `${API_PREFIX}/${CONFIGURATION_API_PREFIX}/{resourceName}/{id}`,
      validate: {
        params: schema.object({
          resourceName: schema.string(),
          id: schema.string({
            minLength: 1,
          }),
        }),
      },
    },
    async (
      context,
      request,
      response
    ): Promise<IOpenSearchDashboardsResponse<any | ResponseError>> => {
      const client = context.security_plugin.esClient.asScoped(request);
      let esResp;
      try {
        esResp = await client.callAsCurrentUser('opensearch_security.deleteResource', {
          resourceName: request.params.resourceName,
          id: request.params.id,
        });
        return response.ok({
          body: {
            message: esResp.message,
          },
        });
      } catch (error) {
        return errorResponse(response, error);
      }
    }
  );

  /**
   * Update object with out Id. Resource identification is expected to computed from headers. Eg: auth headers
   *
   * Request sample:
   * /configuration/account
   * {
   *   "password": "new-password",
   *   "current_password": "old-password"
   * }
   */
  router.post(
    {
      path: `${API_PREFIX}/${CONFIGURATION_API_PREFIX}/{resourceName}`,
      validate: {
        params: schema.object({
          resourceName: schema.string(),
        }),
        body: schema.any(),
      },
    },
    async (
      context,
      request,
      response
    ): Promise<IOpenSearchDashboardsResponse<any | ResponseError>> => {
      try {
        validateRequestBody(request.params.resourceName, request.body);
      } catch (error) {
        return response.badRequest({ body: error });
      }
      const client = context.security_plugin.esClient.asScoped(request);
      let esResp;
      try {
        esResp = await client.callAsCurrentUser('opensearch_security.saveResourceWithoutId', {
          resourceName: request.params.resourceName,
          body: request.body,
        });
        return response.ok({
          body: {
            message: esResp.message,
          },
        });
      } catch (error) {
        return errorResponse(response, error);
      }
    }
  );

  /**
   * Update entity by Id.
   */
  router.post(
    {
      path: `${API_PREFIX}/${CONFIGURATION_API_PREFIX}/{resourceName}/{id}`,
      validate: {
        params: schema.object({
          resourceName: schema.string(),
          id: schema.string({
            validate: validateEntityId,
          }),
        }),
        body: schema.any(),
      },
    },
    async (
      context,
      request,
      response
    ): Promise<IOpenSearchDashboardsResponse<any | ResponseError>> => {
      try {
        validateRequestBody(request.params.resourceName, request.body);
      } catch (error) {
        return response.badRequest({ body: error });
      }
      const client = context.security_plugin.esClient.asScoped(request);
      let esResp;
      try {
        esResp = await client.callAsCurrentUser('opensearch_security.saveResource', {
          resourceName: request.params.resourceName,
          id: request.params.id,
          body: request.body,
        });
        return response.ok({
          body: {
            message: esResp.message,
          },
        });
      } catch (error) {
        return errorResponse(response, error);
      }
    }
  );

  /**
   * Gets authentication info of the user.
   *
   * The response looks like:
   * {
   *   "user": "User [name=admin, roles=[], requestedTenant=__user__]",
   *   "user_name": "admin",
   *   "user_requested_tenant": "__user__",
   *   "remote_address": "127.0.0.1:35044",
   *   "backend_roles": [],
   *   "custom_attribute_names": [],
   *   "roles": ["all_access", "security_manager"],
   *   "tenants": {
   *     "another_tenant": true,
   *     "admin": true,
   *     "global_tenant": true,
   *     "aaaaa": true,
   *     "test tenant": true
   *   },
   *   "principal": null,
   *   "peer_certificates": "0",
   *   "sso_logout_url": null
   * }
   */
  router.get(
    {
      path: `${API_PREFIX}/auth/authinfo`,
      validate: false,
    },
    async (
      context,
      request,
      response
    ): Promise<IOpenSearchDashboardsResponse<any | ResponseError>> => {
      const client = context.security_plugin.esClient.asScoped(request);
      let esResp;
      try {
        esResp = await client.callAsCurrentUser('opensearch_security.authinfo');

        return response.ok({
          body: esResp,
        });
      } catch (error) {
        return errorResponse(response, error);
      }
    }
  );

  /**
   * Gets audit log configuration。
   *
   * Sample payload:
   * {
   *   "enabled":true,
   *   "audit":{
   *     "enable_rest":false,
   *     "disabled_rest_categories":[
   *       "FAILED_LOGIN",
   *       "AUTHENTICATED"
   *     ],
   *     "enable_transport":true,
   *     "disabled_transport_categories":[
   *       "GRANTED_PRIVILEGES"
   *     ],
   *     "resolve_bulk_requests":true,
   *     "log_request_body":false,
   *     "resolve_indices":true,
   *     "exclude_sensitive_headers":true,
   *     "ignore_users":[
   *       "admin",
   *     ],
   *     "ignore_requests":[
   *       "SearchRequest",
   *       "indices:data/read/*"
   *     ]
   *   },
   *   "compliance":{
   *     "enabled":true,
   *     "internal_config":false,
   *     "external_config":false,
   *     "read_metadata_only":false,
   *     "read_watched_fields":{
   *       "indexName1":[
   *         "field1",
   *         "fields-*"
   *       ]
   *     },
   *     "read_ignore_users":[
   *       "opensearchdashboardsserver",
   *       "operator/*"
   *     ],
   *     "write_metadata_only":false,
   *     "write_log_diffs":false,
   *     "write_watched_indices":[
   *       "indexName2",
   *       "indexPatterns-*"
   *     ],
   *     "write_ignore_users":[
   *       "admin"
   *     ]
   *   }
   * }
   */
  router.get(
    {
      path: `${API_PREFIX}/configuration/audit`,
      validate: false,
    },
    async (
      context,
      request,
      response
    ): Promise<IOpenSearchDashboardsResponse<any | ResponseError>> => {
      const client = context.security_plugin.esClient.asScoped(request);

      let esResp;
      try {
        esResp = await client.callAsCurrentUser('opensearch_security.getAudit');

        return response.ok({
          body: esResp,
        });
      } catch (error) {
        return response.custom({
          statusCode: error.statusCode,
          body: parseEsErrorResponse(error),
        });
      }
    }
  );

  /**
   * Update audit log configuration。
   *
   * Sample payload:
   * {
   *   "enabled":true,
   *   "audit":{
   *     "enable_rest":false,
   *     "disabled_rest_categories":[
   *       "FAILED_LOGIN",
   *       "AUTHENTICATED"
   *     ],
   *     "enable_transport":true,
   *     "disabled_transport_categories":[
   *       "GRANTED_PRIVILEGES"
   *     ],
   *     "resolve_bulk_requests":true,
   *     "log_request_body":false,
   *     "resolve_indices":true,
   *     "exclude_sensitive_headers":true,
   *     "ignore_users":[
   *       "admin",
   *     ],
   *     "ignore_requests":[
   *       "SearchRequest",
   *       "indices:data/read/*"
   *     ]
   *   },
   *   "compliance":{
   *     "enabled":true,
   *     "internal_config":false,
   *     "external_config":false,
   *     "read_metadata_only":false,
   *     "read_watched_fields":{
   *       "indexName1":[
   *         "field1",
   *         "fields-*"
   *       ]
   *     },
   *     "read_ignore_users":[
   *       "kibanaserver",
   *       "operator/*"
   *     ],
   *     "write_metadata_only":false,
   *     "write_log_diffs":false,
   *     "write_watched_indices":[
   *       "indexName2",
   *       "indexPatterns-*"
   *     ],
   *     "write_ignore_users":[
   *       "admin"
   *     ]
   *   }
   * }
   */
  router.post(
    {
      path: `${API_PREFIX}/configuration/audit/config`,
      validate: {
        body: schema.any(),
      },
    },
    async (context, request, response) => {
      const client = context.security_plugin.esClient.asScoped(request);
      let esResp;
      try {
        esResp = await client.callAsCurrentUser('opensearch_security.saveAudit', {
          body: request.body,
        });
        return response.ok({
          body: {
            message: esResp.message,
          },
        });
      } catch (error) {
        return errorResponse(response, error);
      }
    }
  );

  /**
   * Deletes cache.
   *
   * Sample response: {"message":"Cache flushed successfully."}
   */
  router.delete(
    {
      path: `${API_PREFIX}/configuration/cache`,
      validate: false,
    },
    async (context, request, response) => {
      const client = context.security_plugin.esClient.asScoped(request);
      let esResponse;
      try {
        esResponse = await client.callAsCurrentUser('opensearch_security.clearCache');
        return response.ok({
          body: {
            message: esResponse.message,
          },
        });
      } catch (error) {
        return errorResponse(response, error);
      }
    }
  );

  /**
   * Gets permission info of current user.
   *
   * Sample response:
   * {
   *   "user": "User [name=admin, roles=[], requestedTenant=__user__]",
   *   "user_name": "admin",
   *   "has_api_access": true,
   *   "disabled_endpoints": {}
   * }
   */
  router.get(
    {
      path: `${API_PREFIX}/restapiinfo`,
      validate: false,
    },
    async (context, request, response) => {
      const client = context.security_plugin.esClient.asScoped(request);
      try {
        const esResponse = await client.callAsCurrentUser('opensearch_security.restapiinfo');
        return response.ok({
          body: esResponse,
        });
      } catch (error) {
        return response.badRequest({
          body: error,
        });
      }
    }
  );

  /**
   * Validates DLS (document level security) query.
   *
   * Request payload is an ES query.
   */
  router.post(
    {
      path: `${API_PREFIX}/${CONFIGURATION_API_PREFIX}/validatedls/{indexName}`,
      validate: {
        params: schema.object({
          // in legacy plugin implmentation, indexName is not used when calling ES API.
          indexName: schema.maybe(schema.string()),
        }),
        body: schema.any(),
      },
    },
    async (context, request, response) => {
      const client = context.security_plugin.esClient.asScoped(request);
      try {
        const esResponse = await client.callAsCurrentUser('opensearch_security.validateDls', {
          body: request.body,
        });
        return response.ok({
          body: esResponse,
        });
      } catch (error) {
        return errorResponse(response, error);
      }
    }
  );

  /**
   * Gets index mapping.
   *
   * Calling ES _mapping API under the hood. see
   * https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-mapping.html
   */
  router.post(
    {
      path: `${API_PREFIX}/${CONFIGURATION_API_PREFIX}/index_mappings`,
      validate: {
        body: schema.object({
          index: schema.arrayOf(schema.string()),
        }),
      },
    },
    async (context, request, response) => {
      const client = context.security_plugin.esClient.asScoped(request);
      try {
        const esResponse = await client.callAsCurrentUser('opensearch_security.getIndexMappings', {
          index: request.body.index.join(','),
          ignore_unavailable: true,
          allow_no_indices: true,
        });

        return response.ok({
          body: esResponse,
        });
      } catch (error) {
        return errorResponse(response, error);
      }
    }
  );

  /**
   * Gets all indices, and field mappings.
   *
   * Calls ES API '/_all/_mapping/field/*' under the hood. see
   * https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-mapping.html
   */
  router.get(
    {
      path: `${API_PREFIX}/${CONFIGURATION_API_PREFIX}/indices`,
      validate: false,
    },
    async (context, request, response) => {
      const client = context.security_plugin.esClient.asScoped(request);
      try {
        const esResponse = await client.callAsCurrentUser('opensearch_security.indices');
        return response.ok({
          body: esResponse,
        });
      } catch (error) {
        return errorResponse(response, error);
      }
    }
  );
}