opensearch-project / OpenSearch-Dashboards

📊 Open source visualization dashboards for OpenSearch.
https://opensearch.org/docs/latest/dashboards/index/
Apache License 2.0
1.66k stars 871 forks source link

Confusing opensearch dashboard import API #1723

Open charlielj88 opened 2 years ago

charlielj88 commented 2 years ago

Hi,

I am using metricbeat-oss version 7.10.2 with aws opensearch 1.2 (compatibility mode enabled so trick the metricbeat with version 7.10.2). The opensearch connection is working fine. However, when I tried to follow the workaround approach in another post (https://github.com/opensearch-project/OpenSearch-Dashboards/issues/831) to manually curl to import the pre-existing metricbeat dashboards to opensearch, I could not trigger the API call successfully,

curl -k -XPOST -u {id}:{password} -H "osd-xsrf: true" -H 'content-type: application/json' \ https://{opensearch url}:443/_dashboards/api/opensearch-dashboards/dashboards/import?exclude=index-patterh\&force=true \ -d@Metricbeat-kubernetes-overview.json

This API is quite puzzling to me, 1) when I executed the curl command, I get a "no matches found" error, and it seems the api does not exist. Not sure it's a version problem. 2) I could not find any documentation on this opensearch dashbaord API online including opensearch website 3) The dashboard in the api is in json format, however, opensearch dashbaord or kibana bashboard only accepts ndjson. How can this work?

Thank you. Charlie

kavilla commented 2 years ago

Hello @charlielj88,

Thank you for opening and calling out the lack of documentation.

We should re-route this to the documentation repo for the site to be updated with the information. If you have an .ndjson file it means should use the saved_objects API. For example:

curl -X POST "http://localhost:5601/api/saved_objects/_import?overwrite=true" -H "osd-xsrf: true" --form file=@foobar.ndjson

^ This supports uploading a ndjson file. I also believe it takes a json file as well.

So from your example:

curl -X POST "http://localhost:5601/api/saved_objects/_import?overwrite=true" -H "osd-xsrf: true" -k -u {id}:{password} --form file=@Metricbeat-kubernetes-overview.json

From the original post they most likely exported via the dashboards/export, from the legacy application that API (and the import) was planned for deprecation.

I understand that the base you have defined for OpenSearch Dashboards is _dashboards. I do see you have {opensearch url}:443. Is that the port you are exposing? Are you able to get the {opensearch url}:443/_dashboards/api/status? If not then either the port is not exposed for you or the user you are using does not have permissions for this API.

If you are able to utilize the saved_objects API then let me know and we can re-route this to the doc repo for them to add information on using this API.

Thanks!

charlielj88 commented 2 years ago

Hello @kavilla,

Thank you for your clarification, since I am using aws managed opensearch, the default opensearch dashboard url is appended with "/_dashboards". Yes I have tested port 443 is open and working.

I have tested "https://{aws_opensearch_url}/_dashboards/api/status" and it could successfully return me the correct response.

I have amended my original curl command accordingly as below,

curl -X POST "https://{aws_opensearch_url}/_dashboards/api/saved_objects/_import?overwrite=true" -H "osd-xsrf: true" -k -u {username}:{password} --form file=@Metricbeat-kubernetes-overview.json

However, I get the return response of "{"statusCode":401,"error":"Unauthorized","message":"Authentication required"}"

The {username} and {password} in the curl command are the ones I can use to login to opensearch dashboard. Is there anything I missed here?

Charlie

kavilla commented 2 years ago

Hello @charlielj88,

Do you know if the user you are using has write access?

charlielj88 commented 2 years ago

Hello @charlielj88,

Do you know if the user you are using has write access?

Hi @kavilla, yes, this is confirmed, as I use the same opensearch credentials for metricbeat to write into the index.

Charlie

charlielj88 commented 2 years ago

@kavilla, I also noticed that if I directly import the saved_object from UI, I have the following error, which seems the json format is not supported,

image
abuwarez commented 2 years ago

Hello. I'm migrating from kibana spaces to opensearch-dashboards with multitenancy and i need a rest api to import dashboards for individual tenants.

api/saved-objects/_import/ is not taking ?security_tenant=tenant_one

running curl -k -XPOST -H 'osd-xsrf: true' 'http://admin:admin@localhost:5601/api/saved_objects/_import/?security_tenant=tenant_one' -d "$(cat kibana_dashboard_one.json)" yields {"statusCode":400,"error":"Bad Request","message":"[request query.security_tenant]: definition for this key is missing"}

tenant is configured:

  "global_tenant": {
    "reserved": true,
    "hidden": false,
    "description": "Global tenant",
    "static": true
  },
  "tenant_one": {
    "reserved": false,
    "hidden": false,
    "description": "tenant one",
    "static": false
  },
  "admin_tenant": {
    "reserved": false,
    "hidden": false,
    "description": "Demo tenant for admin user",
    "static": false
  }
}

Any hints please on importing a dashboard for a specific tenant? Thank you.

charlielj88 commented 2 years ago

@kavilla, I also noticed that if I directly import the saved_object from UI, I have the following error, which seems the json format is not supported,

image

Finally find a way to convert the json to ndjson and now at least I could import metricbeat dashboards from UI...

abuwarez commented 2 years ago

Hello everybody,

Managed to get it working. Below the steps:

Prerequisites:

  1. create tenants using opensearch documented REST API
    # curl -k -XPUT -H'content-type: application/json' https://admin:admin@localhost:9200/_plugins/_security/api/tenants/tenant_one -d '{"description": "tenant one"}'
    {"status":"CREATED","message":"'tenant_one' created."}
    # curl -k -XPUT -H'content-type: application/json' https://admin:admin@localhost:9200/_plugins/_security/api/tenants/tenant_two -d '{"description": "tenant two"}'
    {"status":"CREATED","message":"'tenant_two' created."} 
  2. login to opensearch-dashboards and save the cookie. note the current tenant is __user__, i guess the value for private
    
    # curl -k -XGET -u 'admin:admin' -c dashboards_cookie http://localhost:5601/api/login/

curl -k -XGET -b dashboards_cookie http://localhost:5601/api/v1/configuration/account | jq

{ "data": { "user_name": "admin", "user_requested_tenant": "user", [...] } }

3. switch tenant. note the tenant is kept inside the cookie so we need to save it after this request
```bash
# curl -k -XPOST -b dashboards_cookie -c dashboards_cookie -H'osd-xsrf: true' -H'content-type: application/json' http://localhost:5601/api/v1/multitenancy/tenant -d '{"tenant": "tenant_one", "username": "admin"}'

# curl -k -XGET -b dashboards_cookie http://localhost:5601/api/v1/configuration/account | jq
{
  "data": {
    "user_name": "admin",
    "user_requested_tenant": "tenant_one",
[...]
  }
}
  1. push the dashboard using the same cookie
    
    # curl -k -XPOST -H'osd-xsrf: true' -b dashboards_cookie http://localhost:5601/api/saved_objects/_import?overwrite=true --form file=@export.ndjson | jq
    {
    "successCount": 3,
    "success": true,
    "successResults": [
    {
      "type": "index-pattern",
      "id": "96f0ec20-ec90-11ec-9191-4dc9d4cc1f7d",
      "meta": {
        "title": "*__dobby_docs",
        "icon": "indexPatternApp"
      }
    },
    {
      "type": "visualization",
      "id": "e4aca2a0-ed6d-11ec-9191-4dc9d4cc1f7d",
      "meta": {
        "title": "cucu_dash",
        "icon": "visualizeApp"
      }
    },
    {
      "type": "dashboard",
      "id": "edeca590-ed6d-11ec-9191-4dc9d4cc1f7d",
      "meta": {
        "title": "cucu_the_dash",
        "icon": "dashboardApp"
      }
    }
    ]
    }


Reagrds, C.
rrnair commented 2 years ago

Any help/suggestion is highly appreciated.

I am having trouble importing .ndjson files using SavedObject API within a Lambda, what I am trying is to import a bunch of .ndjson files (dashboards, index-templates etc) at the time of creating a stack (via CDK) that includes Opensearch & Dashboard services. The Opensearch is configured for Cognito authentication and browser logins are working fine. However the lambda (with write access/role to Opensearch domain) is having trouble with API, https://${domain}/_dashboards/api/saved_objects/_import?overwrite=true, the execution fails with a HTML response (guess this is a cognito sign-in form). The request is signed with Sigv4 (@aws-sdk/signature-v4), I checked the https://${domain}/_dashboards/api/status and it works fine.

Does Opensearch dashboard service supports Lambda IAM role to access, I know Opensearch supports read/write from a Lambda using IAM role.

I am reading AWS developer guide - "Loading credentials for a Node.js Lambda function" (https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-lambda.html)

The execution role provides the Lambda function with the credentials it needs to run and to invoke other web services. As a result, you don't need to provide credentials to the Node.js code you write within a Lambda function.

The request has the headers set (not sure whether I am missing any header or Dashboard is looking for a specific header name)

{ "method":"POST", "hostname":"xxxxx-domain", "query":{}, "headers":{ "osd-xsrf":"true", "securitytenant":"Global", "accept":"*/*", "host":"xxxxx-domain", "Content-Length":"1221608", "x-amz-date":"20220927T114706Z", "x-amz-security-token":"IQa....", "x-amz-content-sha256":"a5067....", "authorization":"AWS4-HMAC-SHA256 Credential=AS.../20220927/us-east-1/es/aws4_request, SignedHeaders=accept;content-length;host;osd-xsrf;securitytenant;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=789..." } }

subramaniant06 commented 1 year ago

Any help/suggestion is highly appreciated.

I am having trouble importing .ndjson files using SavedObject API within a Lambda, what I am trying is to import a bunch of .ndjson files (dashboards, index-templates etc) at the time of creating a stack (via CDK) that includes Opensearch & Dashboard services. The Opensearch is configured for Cognito authentication and browser logins are working fine. However the lambda (with write access/role to Opensearch domain) is having trouble with API, https://${domain}/_dashboards/api/saved_objects/_import?overwrite=true, the execution fails with a HTML response (guess this is a cognito sign-in form). The request is signed with Sigv4 (@aws-sdk/signature-v4), I checked the https://${domain}/_dashboards/api/status and it works fine.

Does Opensearch dashboard service supports Lambda IAM role to access, I know Opensearch supports read/write from a Lambda using IAM role.

I am reading AWS developer guide - "Loading credentials for a Node.js Lambda function" (https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-lambda.html)

The execution role provides the Lambda function with the credentials it needs to run and to invoke other web services. As a result, you don't need to provide credentials to the Node.js code you write within a Lambda function.

The request has the headers set (not sure whether I am missing any header or Dashboard is looking for a specific header name)

{ "method":"POST", "hostname":"xxxxx-domain", "query":{}, "headers":{ "osd-xsrf":"true", "securitytenant":"Global", "accept":"*/*", "host":"xxxxx-domain", "Content-Length":"1221608", "x-amz-date":"20220927T114706Z", "x-amz-security-token":"IQa....", "x-amz-content-sha256":"a5067....", "authorization":"AWS4-HMAC-SHA256 Credential=AS.../20220927/us-east-1/es/aws4_request, SignedHeaders=accept;content-length;host;osd-xsrf;securitytenant;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=789..." } }

You need to include the lambda execution role as 'Backend Role' in the corresponding Opensearch Dashboard Role.

rrnair commented 1 year ago

Apologise @subramaniant06 , I failed to notice your response

I do have Lambda role mapped to all_access backend role and this is also done via CDK code just after creating the domain and dashboard.

let requests: object[] = [{ method: 'PUT', path: '_plugins/_security/api/rolesmapping/all_access', body: { backend_roles: [ adminUserRoleArn, lambdaRoleArn ], hosts: [], users: [], } }]

That snippet may be confusing, but what I am doing is to configure roles in opensearch via set of requests thats executed via a CustomResource.

subramaniant06 commented 1 year ago

How are you making this API call? from lambda? This is possible only if you assume the master user role that you used to enable the Fine grained access for Opensearch.

let requests: object[] = [{ method: 'PUT', path: '_plugins/_security/api/rolesmapping/all_access', body: { backend_roles: [ adminUserRoleArn, lambdaRoleArn ], hosts: [], users: [], } }]

I'm able to create tenants, roles, monitors, destinations etc using Custom Resource Lambda with master user role as execution role, AWS NodeHttpClient with Sigv4. However i'm not able to do the multi-part upload using NodeHttpClient to import the index patterns, dashboards and visualizations.

rrnair commented 1 year ago

Guess we are in the same juncture of issue. The lambda execution role is set as masterUserArn in fineGrainedAccessControl of domain. I use Axios to wire roles/rolesmapping (which works fine) and multi-part upload of index patterns/dashboard/visualizations are failing for me as well with a HTML response.

The lambda that imports index-patterns/dashboards etc are triggered from S3 and creates an Axios multi-part request to import savedObjects. (this lambda has the same IAM role as the one that works with CustomResource). I am not sure what is happening in the background of Opensearch and unable to troubleshoot.

Below is the typescript code that handles CustomResource request and index-pattern import request,

public async execute(
        signer: SignatureV4, domain: string, apiPath: string, httpMethod: string, payload: any, 
        securityTenant?: string, filename?: string): Promise<AxiosResponse<any, any>> {

        const method = httpMethod.toLowerCase();

        // Payload is the body of request, incase of a file, we append with boundary and content-type
        let payloads = [];

        // Setup headers
        let headers: Record<string, string> = {};

        if (! apiPath.startsWith('/')) {
            apiPath = `/${apiPath}`;
        }

        const url:URL = new URL(domain + apiPath);

        const endpoint:string = url.hostname;

        // Is this a OS plugin request ?
        if (apiPath.startsWith('/_dashboards')) {
            // Add XSRF request header
            headers["osd-xsrf"] = "true";
        }

        // Add security tenant header 
        if (securityTenant) {
            headers["securitytenant"] = securityTenant;
        }

        // Is this a file upload ?
        if (filename) {
            const name = path.basename(filename);
            headers['accept'] = '*/*';
            const boundary = '----------------------------Telemetry';
            payloads.push(`Content-Type: multipart/form-data; boundary=${boundary}`);
            payloads.push(boundary);
            payloads.push(`Content-Disposition: form-data; name=”file”; filename=”${name}”;`);
            payloads.push('Content-Type: application/x-ndjson');
            payloads.push(payload);
            payloads.push(`${boundary}--`);

        } else {
            // We treat all body as application/json content-type
            headers["Content-Type"] = "application/json";

            // Is payload a string type
            if (typeof payload === "string") {
                payloads.push(payload); 
            } else if  (typeof payload === "object") {
                // Convert object to string
                payloads.push(JSON.stringify(payload));
            } 
        }

        // Concatenate all payload
        const body:string = payloads.join("\r\n");

        // Set host header with hostname
        headers["host"] = url.hostname;
        headers["Content-Length"] = `${Buffer.byteLength(body)}`;

        // Construct http request instance
        const httpRequest = new HttpRequest({
            headers: headers,
            path: apiPath,
            method: httpMethod,
            hostname: endpoint,
            body: body
        });

        // Sign the http request
        const signedRequest = await signer.sign(httpRequest, { signingDate: new Date().toUTCString()});

        const config = { timeout: 60000, headers: signedRequest.headers, httpsAgent: agent };

        if (method === 'put') {
            // Use axios to issue request
            return axios.put(url.toString(), signedRequest.body, config);
        } else if (method === 'post') {
            return axios.post(url.toString(), signedRequest.body, config);
        } else {
            throw new Error(`Unknown HTTP method or method ${method} not supported yet`);
        }
    }

by the way, I am using below API path for importing savedobjects, hope this is correct.

'/_dashboards/api/saved_objects/_import?overwrite=true';

nickchadwick-noaa commented 1 year ago

Hello everybody,

Managed to get it working. Below the steps:

Prerequisites:

  • opensearch 2.0.0 running on localhost:9200 with demo admin user and all required settings for multitenancy
  • openseach-dashboards 2.0.0 running on localhost:5601
  • an exported dashboard to a file. mine was called export.ndjson
  1. create tenants using opensearch documented REST API
# curl -k -XPUT -H'content-type: application/json' https://admin:admin@localhost:9200/_plugins/_security/api/tenants/tenant_one -d '{"description": "tenant one"}'
{"status":"CREATED","message":"'tenant_one' created."}
# curl -k -XPUT -H'content-type: application/json' https://admin:admin@localhost:9200/_plugins/_security/api/tenants/tenant_two -d '{"description": "tenant two"}'
{"status":"CREATED","message":"'tenant_two' created."} 
  1. login to opensearch-dashboards and save the cookie. note the current tenant is __user__, i guess the value for private
# curl -k -XGET -u 'admin:admin' -c dashboards_cookie http://localhost:5601/api/login/

# curl -k -XGET -b dashboards_cookie http://localhost:5601/api/v1/configuration/account | jq
{
  "data": {
    "user_name": "admin",
    "user_requested_tenant": "__user__",
[...]
  }
}
  1. switch tenant. note the tenant is kept inside the cookie so we need to save it after this request
# curl -k -XPOST -b dashboards_cookie -c dashboards_cookie -H'osd-xsrf: true' -H'content-type: application/json' http://localhost:5601/api/v1/multitenancy/tenant -d '{"tenant": "tenant_one", "username": "admin"}'

# curl -k -XGET -b dashboards_cookie http://localhost:5601/api/v1/configuration/account | jq
{
  "data": {
    "user_name": "admin",
    "user_requested_tenant": "tenant_one",
[...]
  }
}
  1. push the dashboard using the same cookie
# curl -k -XPOST -H'osd-xsrf: true' -b dashboards_cookie http://localhost:5601/api/saved_objects/_import?overwrite=true --form file=@export.ndjson | jq
{
  "successCount": 3,
  "success": true,
  "successResults": [
    {
      "type": "index-pattern",
      "id": "96f0ec20-ec90-11ec-9191-4dc9d4cc1f7d",
      "meta": {
        "title": "*__dobby_docs",
        "icon": "indexPatternApp"
      }
    },
    {
      "type": "visualization",
      "id": "e4aca2a0-ed6d-11ec-9191-4dc9d4cc1f7d",
      "meta": {
        "title": "cucu_dash",
        "icon": "visualizeApp"
      }
    },
    {
      "type": "dashboard",
      "id": "edeca590-ed6d-11ec-9191-4dc9d4cc1f7d",
      "meta": {
        "title": "cucu_the_dash",
        "icon": "dashboardApp"
      }
    }
  ]
}

Reagrds, C.

Hello,

I was able to achieve using the saved_objects API with this method, except at step 2 where I had to use a slightly different request to login and get the cookie (I am using AWS OpenSearch 2.3, so the Dashboards API endpoint starts with _dashboards):

curl -XPOST -c dashboards_cookie https://${os_endpoint}/_dashboards/auth/login -H "osd-xsrf: true" -H "Content-Type: application/json" -d '{"username":"${admin_username}","password":"${admin_password}"}'

Thanks, Nick

LHozzan commented 1 year ago

Hi guys.

Today I discovered another very confusing thing with importing dashboard to specific tenant.

If I make tenant NGSupport (for example) and use credentials for OpenSearch Dashboard admin account, API return success, but it import not to the NGSupport tenant, but to the Global tenant.

$ curl -k -w ", HTTP:%{http_code}" -X POST -uadmin:REDACTED "http://localhost:5601/api/saved_objects/_import?overwrite=true" -H "securitytenant: NGSupport" -H "osd-xsrf: true" --form file=@TestDashboard.ndjson
{"successCount":3,"success":true,"successResults":[{"type":"index-pattern","id":"nginx-*","meta":{"title":"nginx-*","icon":"indexPatternApp"},"overwrite":true},{"type":"search","id":"149258c0-b816-11ed-89d4-ffdda45e5108","meta":{"title":"NginxRequests","icon":"discoverApp"}},{"type":"dashboard","id":"399571c0-b816-11ed-89d4-ffdda45e5108","meta":{"title":"NGSupport Dashboard","icon":"dashboardApp"}}]}, HTTP:200

Workaround for the problem is make dedicated internal user with read/write access to the tenant and using these credentials for importing.

I expecting, that:

If I login to OpenSearch Dashboard as admin, via GUI I can import any objects to any tenants without any problems. Why API calls are different?

Outrun207 commented 1 year ago

Hello, this still seems to be an issue in OS 2.7.

I am trying to get a saved object up with the following method:

def import_saved_objects(os_endpoint_url, os_pass):
    file_name = "security-lake-opensearch.ndjson"
    with open(f"os_configuration_templates/{file_name}", encoding="utf-8") as f:
        data = f.read()
    path = f"{os_endpoint_url}/_dashboards/api/saved_objects/_import?createNewCopies=true"
    headers = {"Content-Type": "application/json", "osd-xsrf": "true"}
    with tempfile.NamedTemporaryFile("w+t") as f:
        f.name = file_name
        f.write(data)
        f.seek(0)
        response = requests.post(
            path,
            headers=headers,
            auth = ("admin", os_pass),
            timeout=90,
            files={"file": f},
        )
        print(response)

The response is a 401 code from Opensearch. This file uploads fine from the gui.