View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.syncope.ext.opensearch.client;
20  
21  import com.fasterxml.jackson.databind.JsonNode;
22  import java.io.IOException;
23  import java.util.List;
24  import java.util.Map;
25  import org.apache.syncope.common.lib.types.AnyTypeKind;
26  import org.apache.syncope.core.persistence.api.entity.Any;
27  import org.apache.syncope.core.persistence.api.entity.Entity;
28  import org.apache.syncope.core.persistence.api.entity.Realm;
29  import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent;
30  import org.apache.syncope.core.spring.security.SecureRandomUtils;
31  import org.identityconnectors.framework.common.objects.SyncDeltaType;
32  import org.opensearch.client.opensearch.OpenSearchClient;
33  import org.opensearch.client.opensearch._types.OpenSearchException;
34  import org.opensearch.client.opensearch._types.Refresh;
35  import org.opensearch.client.opensearch._types.analysis.CustomNormalizer;
36  import org.opensearch.client.opensearch._types.analysis.Normalizer;
37  import org.opensearch.client.opensearch._types.mapping.DynamicTemplate;
38  import org.opensearch.client.opensearch._types.mapping.KeywordProperty;
39  import org.opensearch.client.opensearch._types.mapping.ObjectProperty;
40  import org.opensearch.client.opensearch._types.mapping.Property;
41  import org.opensearch.client.opensearch._types.mapping.TextProperty;
42  import org.opensearch.client.opensearch._types.mapping.TypeMapping;
43  import org.opensearch.client.opensearch.core.DeleteRequest;
44  import org.opensearch.client.opensearch.core.DeleteResponse;
45  import org.opensearch.client.opensearch.core.IndexRequest;
46  import org.opensearch.client.opensearch.core.IndexResponse;
47  import org.opensearch.client.opensearch.indices.CreateIndexRequest;
48  import org.opensearch.client.opensearch.indices.CreateIndexResponse;
49  import org.opensearch.client.opensearch.indices.DeleteIndexRequest;
50  import org.opensearch.client.opensearch.indices.DeleteIndexResponse;
51  import org.opensearch.client.opensearch.indices.ExistsRequest;
52  import org.opensearch.client.opensearch.indices.IndexSettings;
53  import org.opensearch.client.opensearch.indices.IndexSettingsAnalysis;
54  import org.slf4j.Logger;
55  import org.slf4j.LoggerFactory;
56  import org.springframework.transaction.event.TransactionalEventListener;
57  
58  /**
59   * Listen to any create / update and delete in order to keep the OpenSearch indexes consistent.
60   */
61  public class OpenSearchIndexManager {
62  
63      private static final Logger LOG = LoggerFactory.getLogger(OpenSearchIndexManager.class);
64  
65      protected final OpenSearchClient client;
66  
67      protected final OpenSearchUtils openSearchUtils;
68  
69      protected final String numberOfShards;
70  
71      protected final String numberOfReplicas;
72  
73      public OpenSearchIndexManager(
74              final OpenSearchClient client,
75              final OpenSearchUtils ppenSearchUtils,
76              final String numberOfShards,
77              final String numberOfReplicas) {
78  
79          this.client = client;
80          this.openSearchUtils = ppenSearchUtils;
81          this.numberOfShards = numberOfShards;
82          this.numberOfReplicas = numberOfReplicas;
83      }
84  
85      public boolean existsAnyIndex(final String domain, final AnyTypeKind kind) throws IOException {
86          return client.indices().exists(new ExistsRequest.Builder().
87                  index(OpenSearchUtils.getAnyIndex(domain, kind)).build()).
88                  value();
89      }
90  
91      public boolean existsRealmIndex(final String domain) throws IOException {
92          return client.indices().exists(new ExistsRequest.Builder().
93                  index(OpenSearchUtils.getRealmIndex(domain)).build()).
94                  value();
95      }
96  
97      public boolean existsAuditIndex(final String domain) throws IOException {
98          return client.indices().exists(new ExistsRequest.Builder().
99                  index(OpenSearchUtils.getAuditIndex(domain)).build()).
100                 value();
101     }
102 
103     public IndexSettings defaultSettings() throws IOException {
104         return new IndexSettings.Builder().
105                 analysis(new IndexSettingsAnalysis.Builder().
106                         normalizer("string_lowercase", new Normalizer.Builder().
107                                 custom(new CustomNormalizer.Builder().
108                                         charFilter(List.of()).
109                                         filter("lowercase").
110                                         build()).
111                                 build()).
112                         build()).
113                 numberOfShards(numberOfShards).
114                 numberOfReplicas(numberOfReplicas).
115                 build();
116     }
117 
118     public TypeMapping defaultAnyMapping() throws IOException {
119         return new TypeMapping.Builder().
120                 dynamicTemplates(List.of(Map.of(
121                         "strings",
122                         new DynamicTemplate.Builder().
123                                 matchMappingType("string").
124                                 mapping(new Property.Builder().
125                                         keyword(new KeywordProperty.Builder().normalizer("string_lowercase").build()).
126                                         build()).
127                                 build()))).
128                 build();
129     }
130 
131     public TypeMapping defaultRealmMapping() throws IOException {
132         return new TypeMapping.Builder().
133                 dynamicTemplates(List.of(Map.of(
134                         "strings",
135                         new DynamicTemplate.Builder().
136                                 matchMappingType("string").
137                                 mapping(new Property.Builder().
138                                         keyword(new KeywordProperty.Builder().normalizer("string_lowercase").build()).
139                                         build()).
140                                 build()))).
141                 build();
142     }
143 
144     public TypeMapping defaultAuditMapping() throws IOException {
145         return new TypeMapping.Builder().
146                 dynamicTemplates(List.of(Map.of(
147                         "strings",
148                         new DynamicTemplate.Builder().
149                                 matchMappingType("string").
150                                 mapping(new Property.Builder().
151                                         keyword(new KeywordProperty.Builder().normalizer("string_lowercase").build()).
152                                         build()).
153                                 build()))).
154                 properties(
155                         "message",
156                         new Property.Builder().object(new ObjectProperty.Builder().
157                                 properties(
158                                         "before",
159                                         new Property.Builder().
160                                                 text(new TextProperty.Builder().analyzer("standard").build()).
161                                                 build()).
162                                 properties(
163                                         "inputs",
164                                         new Property.Builder().
165                                                 text(new TextProperty.Builder().analyzer("standard").build()).
166                                                 build()).
167                                 properties(
168                                         "output",
169                                         new Property.Builder().
170                                                 text(new TextProperty.Builder().analyzer("standard").build()).
171                                                 build()).
172                                 properties(
173                                         "throwable",
174                                         new Property.Builder().
175                                                 text(new TextProperty.Builder().analyzer("standard").build()).
176                                                 build()).
177                                 build()).
178                                 build()).
179                 build();
180     }
181 
182     protected CreateIndexResponse doCreateAnyIndex(
183             final String domain,
184             final AnyTypeKind kind,
185             final IndexSettings settings,
186             final TypeMapping mappings) throws IOException {
187 
188         return client.indices().create(
189                 new CreateIndexRequest.Builder().
190                         index(OpenSearchUtils.getAnyIndex(domain, kind)).
191                         settings(settings).
192                         mappings(mappings).
193                         build());
194     }
195 
196     public void createAnyIndex(
197             final String domain,
198             final AnyTypeKind kind,
199             final IndexSettings settings,
200             final TypeMapping mappings)
201             throws IOException {
202 
203         try {
204             CreateIndexResponse response = doCreateAnyIndex(domain, kind, settings, mappings);
205 
206             LOG.debug("Successfully created {} for {}: {}",
207                     OpenSearchUtils.getAnyIndex(domain, kind), kind.name(), response);
208         } catch (OpenSearchException e) {
209             LOG.debug("Could not create index {} because it already exists",
210                     OpenSearchUtils.getAnyIndex(domain, kind), e);
211 
212             removeAnyIndex(domain, kind);
213             doCreateAnyIndex(domain, kind, settings, mappings);
214         }
215     }
216 
217     public void removeAnyIndex(final String domain, final AnyTypeKind kind) throws IOException {
218         DeleteIndexResponse response = client.indices().delete(
219                 new DeleteIndexRequest.Builder().index(OpenSearchUtils.getAnyIndex(domain, kind)).build());
220         LOG.debug("Successfully removed {}: {}", OpenSearchUtils.getAnyIndex(domain, kind), response);
221     }
222 
223     protected CreateIndexResponse doCreateRealmIndex(
224             final String domain,
225             final IndexSettings settings,
226             final TypeMapping mappings) throws IOException {
227 
228         return client.indices().create(
229                 new CreateIndexRequest.Builder().
230                         index(OpenSearchUtils.getRealmIndex(domain)).
231                         settings(settings).
232                         mappings(mappings).
233                         build());
234     }
235 
236     public void createRealmIndex(
237             final String domain,
238             final IndexSettings settings,
239             final TypeMapping mappings)
240             throws IOException {
241 
242         try {
243             CreateIndexResponse response = doCreateRealmIndex(domain, settings, mappings);
244 
245             LOG.debug("Successfully created realm index {}: {}",
246                     OpenSearchUtils.getRealmIndex(domain), response);
247         } catch (OpenSearchException e) {
248             LOG.debug("Could not create realm index {} because it already exists",
249                     OpenSearchUtils.getRealmIndex(domain), e);
250 
251             removeRealmIndex(domain);
252             doCreateRealmIndex(domain, settings, mappings);
253         }
254     }
255 
256     public void removeRealmIndex(final String domain) throws IOException {
257         DeleteIndexResponse response = client.indices().delete(
258                 new DeleteIndexRequest.Builder().index(OpenSearchUtils.getRealmIndex(domain)).build());
259         LOG.debug("Successfully removed {}: {}", OpenSearchUtils.getRealmIndex(domain), response);
260     }
261 
262     protected CreateIndexResponse doCreateAuditIndex(
263             final String domain,
264             final IndexSettings settings,
265             final TypeMapping mappings) throws IOException {
266 
267         return client.indices().create(
268                 new CreateIndexRequest.Builder().
269                         index(OpenSearchUtils.getAuditIndex(domain)).
270                         settings(settings).
271                         mappings(mappings).
272                         build());
273     }
274 
275     public void createAuditIndex(
276             final String domain,
277             final IndexSettings settings,
278             final TypeMapping mappings)
279             throws IOException {
280 
281         try {
282             CreateIndexResponse response = doCreateAuditIndex(domain, settings, mappings);
283 
284             LOG.debug("Successfully created audit index {}: {}",
285                     OpenSearchUtils.getAuditIndex(domain), response);
286         } catch (OpenSearchException e) {
287             LOG.debug("Could not create audit index {} because it already exists",
288                     OpenSearchUtils.getAuditIndex(domain), e);
289 
290             removeAuditIndex(domain);
291             doCreateAuditIndex(domain, settings, mappings);
292         }
293     }
294 
295     public void removeAuditIndex(final String domain) throws IOException {
296         DeleteIndexResponse response = client.indices().delete(
297                 new DeleteIndexRequest.Builder().index(OpenSearchUtils.getAuditIndex(domain)).build());
298         LOG.debug("Successfully removed {}: {}", OpenSearchUtils.getAuditIndex(domain), response);
299     }
300 
301     @TransactionalEventListener
302     public void entity(final EntityLifecycleEvent<Entity> event) throws IOException {
303         LOG.debug("About to {} index for {}", event.getType().name(), event.getEntity());
304 
305         if (event.getEntity() instanceof Any) {
306             Any<?> any = (Any<?>) event.getEntity();
307 
308             if (event.getType() == SyncDeltaType.DELETE) {
309                 DeleteRequest request = new DeleteRequest.Builder().index(
310                         OpenSearchUtils.getAnyIndex(event.getDomain(), any.getType().getKind())).
311                         id(any.getKey()).
312                         build();
313                 DeleteResponse response = client.delete(request);
314                 LOG.debug("Index successfully deleted for {}[{}]: {}",
315                         any.getType().getKind(), any.getKey(), response);
316             } else {
317                 IndexRequest<Map<String, Object>> request = new IndexRequest.Builder<Map<String, Object>>().
318                         index(OpenSearchUtils.getAnyIndex(event.getDomain(), any.getType().getKind())).
319                         id(any.getKey()).
320                         document(openSearchUtils.document(any)).
321                         build();
322                 IndexResponse response = client.index(request);
323                 LOG.debug("Index successfully created or updated for {}: {}", any, response);
324             }
325         } else if (event.getEntity() instanceof Realm) {
326             Realm realm = (Realm) event.getEntity();
327 
328             if (event.getType() == SyncDeltaType.DELETE) {
329                 DeleteRequest request = new DeleteRequest.Builder().
330                         index(OpenSearchUtils.getRealmIndex(event.getDomain())).
331                         id(realm.getKey()).
332                         refresh(Refresh.True).
333                         build();
334                 DeleteResponse response = client.delete(request);
335                 LOG.debug("Index successfully deleted for {}: {}", realm, response);
336             } else {
337                 IndexRequest<Map<String, Object>> request = new IndexRequest.Builder<Map<String, Object>>().
338                         index(OpenSearchUtils.getRealmIndex(event.getDomain())).
339                         id(realm.getKey()).
340                         document(openSearchUtils.document(realm)).
341                         refresh(Refresh.True).
342                         build();
343                 IndexResponse response = client.index(request);
344                 LOG.debug("Index successfully created or updated for {}: {}", realm, response);
345             }
346         }
347     }
348 
349     public void audit(final String domain, final long instant, final JsonNode message) throws IOException {
350         LOG.debug("About to audit");
351 
352         IndexRequest<Map<String, Object>> request = new IndexRequest.Builder<Map<String, Object>>().
353                 index(OpenSearchUtils.getAuditIndex(domain)).
354                 id(SecureRandomUtils.generateRandomUUID().toString()).
355                 document(openSearchUtils.document(instant, message, domain)).
356                 build();
357         IndexResponse response = client.index(request);
358 
359         LOG.debug("Audit successfully created: {}", response);
360     }
361 }