Skip to content

Commit 0849905

Browse files
authored
Merge pull request #141 from elastic/seanstory/add-spo-dls-to-internal-knowledge-search
add spo dls to internal knowledge search
2 parents 1c233b3 + 2627e4e commit 0849905

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+12601
-807
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FLASK_APP=api/app.py
2+
FLASK_RUN_PORT=3001
3+
FLASK_DEBUG=1
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
frontend/build
2+
frontend/node_modules
3+
api/__pycache__
4+
.venv
5+
venv
6+
.DS_Store
7+
.env
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Elastic Internal Knowledge Search App
2+
3+
This is a sample app that demonstrates how to build an internal knowledge search application with document-level security on top of Elasticsearch.
4+
5+
**Requires at least 8.11.0 of Elasticsearch.**
6+
7+
8+
## Download the Project
9+
10+
Download the project from Github and extract the `internal-knowledge-search` folder.
11+
12+
```bash
13+
curl https://codeload.github.com/elastic/elasticsearch-labs/tar.gz/main | \
14+
tar -xz --strip=2 elasticsearch-labs-main/example-apps/internal-knowledge-search
15+
```
16+
17+
## Installing and connecting to Elasticsearch
18+
19+
### Install Elasticsearch
20+
21+
There are a number of ways to install Elasticsearch. Cloud is best for most use-cases. Visit the [Install Elasticsearch](https://www.elastic.co/search-labs/tutorials/install-elasticsearch) for more information.
22+
23+
### Connect to Elasticsearch
24+
25+
This app requires the following environment variables to be set to connect to Elasticsearch:
26+
27+
```sh
28+
export ELASTICSEARCH_URL=...
29+
export ELASTIC_USERNAME=...
30+
export ELASTIC_PASSWORD=...
31+
```
32+
33+
You can add these to a `.env` file for convenience. See the `env.example` file for a .env file template.
34+
35+
You can also set the `ELASTIC_CLOUD_ID` instead of the `ELASTICSEARCH_URL` if you're connecting to a cloud instance and prefer to use the cloud ID.
36+
37+
# Workplace Search Reference App
38+
39+
This application shows you how to build an application using [Elastic Search Applications](https://www.elastic.co/guide/en/enterprise-search/current/search-applications.html) for a Workplace Search use case.
40+
![img.png](img.png)
41+
42+
The application uses the [Search Application Client](https://github.com/elastic/search-application-client). Refer to this [guide](https://www.elastic.co/guide/en/enterprise-search/current/search-applications-search.html) for more information.
43+
44+
## Running the application
45+
46+
### Configuring mappings (subject to change in the near future)
47+
48+
The application uses two mapping files (will be replaced with a corresponding UI in the near future).
49+
One specifies the mapping of the documents in your indices to the rendered search result.
50+
The other one maps a source index to a corresponding logo.
51+
52+
#### Data mapping
53+
54+
The data mappings are located inside [config/documentsToSearchResultMappings.json](src/config/documentsToSearchResultMappings.json).
55+
Each entry maps the fields of the documents to the search result UI component for a specific index. The mapping expects `title`, `created`, `previewText`, `fullText`, and `link` as keys.
56+
Specify a field name of the document you want to map for each key.
57+
58+
##### Example:
59+
60+
Content document:
61+
62+
````json
63+
{
64+
"name": "Document name",
65+
"_timestamp": "2342345934",
66+
"summary": "Some summary",
67+
"fullText": "description",
68+
"link": "some listing url"
69+
}
70+
````
71+
72+
Mapping:
73+
````json
74+
{
75+
"search-mongo": {
76+
"title": "name",
77+
"created": "_timestamp",
78+
"previewText": "summary",
79+
"fullText": "description",
80+
"link": "listing_url"
81+
}
82+
}
83+
````
84+
85+
#### Logo mapping
86+
You can specify a logo for each index behind the search application. Place your logo inside [data-source-logos](public/data-source-logos) and configure
87+
your mapping as follows:
88+
89+
````json
90+
{
91+
"search-index-1": "data-source-logos/some_logo.png",
92+
"search-index-2": "data-source-logos/some_other_logo.webp"
93+
}
94+
````
95+
96+
### Configuring the search application
97+
98+
To be able to use the index filtering and sorting in the UI you should update the search template of your search application:
99+
100+
`PUT _application/search_application/{YOUR_SEARCH_APPLICATION_NAME}`
101+
````json
102+
{
103+
"indices": [{YOUR_INDICES_USED_BY_THE_SEARCH_APPLICATION}],
104+
"template": {
105+
"script": {
106+
"lang": "mustache",
107+
"source": """
108+
{
109+
"query": {
110+
"bool": {
111+
"must": [
112+
{{#query}}
113+
{
114+
"query_string": {
115+
"query": "{{query}}"
116+
}
117+
}
118+
{{/query}}
119+
],
120+
"filter": {
121+
"terms": {
122+
"_index": {{#toJson}}indices{{/toJson}}
123+
}
124+
}
125+
}
126+
},
127+
"from": {{from}},
128+
"size": {{size}},
129+
"sort": {{#toJson}}sort{{/toJson}}
130+
}
131+
""",
132+
"params": {
133+
"query": "",
134+
"size": 10,
135+
"from": 0,
136+
"sort": [],
137+
"indices": []
138+
}
139+
}
140+
}
141+
````
142+
143+
### Setting the search app variables
144+
145+
You need to set search application name and search application endpoints to the corresponding values in the UI. You'll get these values when [creating a search application](https://www.elastic.co/guide/en/enterprise-search/current/search-applications.html). Note that for the endpoint you should use just the hostname, so excluding the `/_application/search_application/{application_name}/_search`.
146+
147+
### Disable CORS
148+
149+
By default, Elasticsearch is configured to disallow cross-origin resource requests. To call Elasticsearch from the browser, you will need to [enable CORS on your Elasticsearch deployment](https://www.elastic.co/guide/en/elasticsearch/reference/current/behavioral-analytics-cors.html#behavioral-analytics-cors-enable-cors-elasticsearch).
150+
151+
If you don't feel comfortable enabling CORS on your Elasticsearch deployment, you can set the search endpoint in the UI to `http://localhost:3001/api/search_proxy`. Change the host if you're running the backend elsewhere. This will make the backend act as a proxy for the search calls, which is what you're most likely going to do in production.
152+
153+
154+
### Set up DLS with SPO
155+
1. create a connector in kibana named `search-sharepoint`
156+
2. start connectors-python, if using connector clients
157+
3. enable DLS
158+
4. run an access control sync
159+
5. run a full sync
160+
6. define mappings, as above in this README
161+
7. create search application
162+
8. enable cors: https://www.elastic.co/guide/en/elasticsearch/reference/master/search-application-security.html#search-application-security-cors-elasticsearch
163+
164+
### Change your API host
165+
166+
By default, this app will run on `http://localhost:3000` and the backend on `http://localhost:3001`. If you are running the backend in a different location, set the environment variable `REACT_APP_API_HOST` to wherever you're hosting your backend, plus the `/api` path.
167+
168+
169+
### Run API and frontend
170+
171+
```sh
172+
# Launch API app
173+
flask run
174+
175+
# In a separate terminal launch frontend app
176+
cd app-ui && npm install && npm run start
177+
```
178+
179+
You can now access the frontend at http://localhost:3000. Changes are automatically reloaded.
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
from flask import Flask, jsonify, request, Response, current_app
2+
from flask_cors import CORS
3+
from elasticsearch_client import elasticsearch_client
4+
import os
5+
import sys
6+
import requests
7+
8+
app = Flask(__name__, static_folder="../frontend/build", static_url_path="/")
9+
CORS(app)
10+
11+
12+
def get_identities_index(search_app_name):
13+
search_app = elasticsearch_client.search_application.get(
14+
name=search_app_name)
15+
identities_indices = elasticsearch_client.indices.get(
16+
index=".search-acl-filter*")
17+
secured_index = [
18+
app_index
19+
for app_index in search_app["indices"]
20+
if ".search-acl-filter-" + app_index in identities_indices
21+
]
22+
if len(secured_index) > 0:
23+
identities_index = ".search-acl-filter-" + secured_index[0]
24+
return identities_index
25+
else:
26+
raise ValueError(
27+
"Could not find identities index for search application %s", search_app_name
28+
)
29+
30+
31+
@app.route("/")
32+
def api_index():
33+
return app.send_static_file("index.html")
34+
35+
36+
@app.route("/api/default_settings", methods=["GET"])
37+
def default_settings():
38+
return {
39+
"elasticsearch_endpoint": os.getenv("ELASTICSEARCH_URL") or "http://localhost:9200"
40+
}
41+
42+
43+
@app.route("/api/search_proxy/<path:text>", methods=["POST"])
44+
def search(text):
45+
response = requests.request(
46+
method="POST",
47+
url=os.getenv("ELASTICSEARCH_URL") + '/' + text,
48+
data=request.get_data(),
49+
allow_redirects=False,
50+
headers={"Authorization": request.headers.get(
51+
"Authorization"), "Content-Type": "application/json"}
52+
)
53+
54+
return response.content
55+
56+
57+
@app.route("/api/persona", methods=["GET"])
58+
def personas():
59+
try:
60+
search_app_name = request.args.get("app_name")
61+
identities_index = get_identities_index(search_app_name)
62+
response = elasticsearch_client.search(
63+
index=identities_index, size=1000)
64+
hits = response["hits"]["hits"]
65+
personas = [x["_id"] for x in hits]
66+
personas.append("admin")
67+
return personas
68+
69+
except Exception as e:
70+
current_app.logger.warn(
71+
"Encountered error %s while fetching personas, returning default persona", e
72+
)
73+
return ["admin"]
74+
75+
76+
@app.route("/api/indices", methods=["GET"])
77+
def indices():
78+
try:
79+
search_app_name = request.args.get("app_name")
80+
search_app = elasticsearch_client.search_application.get(
81+
name=search_app_name)
82+
return search_app['indices']
83+
84+
except Exception as e:
85+
current_app.logger.warn(
86+
"Encountered error %s while fetching indices, returning no indices", e
87+
)
88+
return []
89+
90+
91+
@app.route("/api/api_key", methods=["GET"])
92+
def api_key():
93+
search_app_name = request.args.get("app_name")
94+
role_name = search_app_name + "-key-role"
95+
default_role_descriptor = {}
96+
default_role_descriptor[role_name] = {
97+
"cluster": [],
98+
"indices": [
99+
{
100+
"names": [search_app_name],
101+
"privileges": ["read"],
102+
"allow_restricted_indices": False,
103+
}
104+
],
105+
"applications": [],
106+
"run_as": [],
107+
"metadata": {},
108+
"transient_metadata": {"enabled": True},
109+
"restriction": {"workflows": ["search_application_query"]},
110+
}
111+
identities_index = get_identities_index(search_app_name)
112+
try:
113+
persona = request.args.get("persona")
114+
if persona == "":
115+
raise ValueError("No persona specified")
116+
role_descriptor = {}
117+
118+
if persona == "admin":
119+
role_descriptor = default_role_descriptor
120+
else:
121+
identity = elasticsearch_client.get(
122+
index=identities_index, id=persona)
123+
permissions = identity["_source"]["query"]["template"]["params"][
124+
"access_control"
125+
]
126+
role_descriptor = {
127+
"dls-role": {
128+
"cluster": ["all"],
129+
"indices": [
130+
{
131+
"names": [search_app_name],
132+
"privileges": ["read"],
133+
"query": {
134+
"template": {
135+
"params": {"access_control": permissions},
136+
"source": """{
137+
"bool": {
138+
"filter": {
139+
"bool": {
140+
"should": [
141+
{
142+
"bool": {
143+
"must_not": {
144+
"exists": {
145+
"field": "_allow_access_control"
146+
}
147+
}
148+
}
149+
},
150+
{
151+
"terms": {
152+
"_allow_access_control.enum": {{#toJson}}access_control{{/toJson}}
153+
}
154+
}
155+
]
156+
}
157+
}
158+
}
159+
}""",
160+
}
161+
},
162+
}
163+
],
164+
"restriction": {"workflows": ["search_application_query"]},
165+
}
166+
}
167+
api_key = elasticsearch_client.security.create_api_key(
168+
name=search_app_name+"-internal-knowledge-search-example-"+persona, expiration="1h", role_descriptors=role_descriptor)
169+
return {"api_key": api_key['encoded']}
170+
171+
except Exception as e:
172+
current_app.logger.warn(
173+
"Encountered error %s while fetching api key", e)
174+
raise e
175+
176+
177+
if __name__ == "__main__":
178+
app.run(port=3001, debug=True)

0 commit comments

Comments
 (0)