|
48 | 48 | import org.elasticsearch.client.RestClient; |
49 | 49 | import org.elasticsearch.client.RestClientBuilder; |
50 | 50 | import org.elasticsearch.client.WarningsHandler; |
| 51 | +import org.elasticsearch.cluster.metadata.Metadata; |
51 | 52 | import org.elasticsearch.common.Strings; |
52 | 53 | import org.elasticsearch.common.bytes.BytesArray; |
53 | 54 | import org.elasticsearch.common.bytes.BytesReference; |
|
74 | 75 | import org.elasticsearch.index.query.QueryBuilder; |
75 | 76 | import org.elasticsearch.index.seqno.ReplicationTracker; |
76 | 77 | import org.elasticsearch.rest.RestStatus; |
| 78 | +import org.elasticsearch.tasks.Task; |
77 | 79 | import org.elasticsearch.test.AbstractBroadcastResponseTestCase; |
78 | 80 | import org.elasticsearch.test.ESTestCase; |
79 | 81 | import org.elasticsearch.test.MapMatcher; |
|
142 | 144 | import static org.elasticsearch.test.rest.TestFeatureService.ALL_FEATURES; |
143 | 145 | import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; |
144 | 146 | import static org.hamcrest.Matchers.anyOf; |
| 147 | +import static org.hamcrest.Matchers.containsInAnyOrder; |
145 | 148 | import static org.hamcrest.Matchers.containsString; |
| 149 | +import static org.hamcrest.Matchers.empty; |
146 | 150 | import static org.hamcrest.Matchers.equalTo; |
147 | 151 | import static org.hamcrest.Matchers.everyItem; |
148 | 152 | import static org.hamcrest.Matchers.greaterThanOrEqualTo; |
@@ -2702,4 +2706,137 @@ protected static void assertResultMap( |
2702 | 2706 | ) { |
2703 | 2707 | assertMap(result, mapMatcher.entry("columns", columnMatcher).entry("values", valuesMatcher)); |
2704 | 2708 | } |
| 2709 | + |
| 2710 | + protected void createProject(String project) throws IOException { |
| 2711 | + assert multiProjectEnabled; |
| 2712 | + RestClient client = adminClient(); |
| 2713 | + final Request request = new Request("PUT", "/_project/" + project); |
| 2714 | + try { |
| 2715 | + final Response response = client.performRequest(request); |
| 2716 | + logger.info("Created project {} : {}", project, response.getStatusLine()); |
| 2717 | + } catch (ResponseException e) { |
| 2718 | + logger.error("Failed to create project: {}", project); |
| 2719 | + throw e; |
| 2720 | + } |
| 2721 | + } |
| 2722 | + |
| 2723 | + protected void assertProjectIds(RestClient client, List<String> expectedProjects) throws IOException { |
| 2724 | + assert multiProjectEnabled; |
| 2725 | + final Collection<String> actualProjects = getProjectIds(client); |
| 2726 | + assertThat( |
| 2727 | + "Cluster returned project ids: " + actualProjects, |
| 2728 | + actualProjects, |
| 2729 | + containsInAnyOrder(expectedProjects.toArray(String[]::new)) |
| 2730 | + ); |
| 2731 | + } |
| 2732 | + |
| 2733 | + private Collection<String> getProjectIds(RestClient client) throws IOException { |
| 2734 | + assert multiProjectEnabled; |
| 2735 | + final Request request = new Request("GET", "/_cluster/state/routing_table?multi_project=true"); |
| 2736 | + try { |
| 2737 | + final ObjectPath response = ObjectPath.createFromResponse(client.performRequest(request)); |
| 2738 | + final List<Map<String, Object>> projectRouting = response.evaluate("routing_table.projects"); |
| 2739 | + return projectRouting.stream().map(obj -> (String) obj.get("id")).toList(); |
| 2740 | + } catch (ResponseException e) { |
| 2741 | + logger.error("Failed to retrieve cluster state"); |
| 2742 | + throw e; |
| 2743 | + } |
| 2744 | + } |
| 2745 | + |
| 2746 | + protected void assertEmptyProject(String projectId) throws IOException { |
| 2747 | + assert multiProjectEnabled; |
| 2748 | + final Request request = new Request("GET", "_cluster/state/metadata,routing_table,customs"); |
| 2749 | + request.setOptions(request.getOptions().toBuilder().addHeader(Task.X_ELASTIC_PROJECT_ID_HTTP_HEADER, projectId).build()); |
| 2750 | + |
| 2751 | + var response = responseAsMap(adminClient().performRequest(request)); |
| 2752 | + ObjectPath state = new ObjectPath(response); |
| 2753 | + |
| 2754 | + final var indexNames = ((Map<?, ?>) state.evaluate("metadata.indices")).keySet(); |
| 2755 | + final var routingTableEntries = ((Map<?, ?>) state.evaluate("routing_table.indices")).keySet(); |
| 2756 | + if (indexNames.isEmpty() == false || routingTableEntries.isEmpty() == false) { |
| 2757 | + // Only the default project is allowed to have the security index after tests complete. |
| 2758 | + // The security index could show up in the indices, routing table, or both. |
| 2759 | + // If that happens, we need to check that it hasn't been modified by any leaking API calls. |
| 2760 | + if (projectId.equals(Metadata.DEFAULT_PROJECT_ID.id()) |
| 2761 | + && (indexNames.isEmpty() || (indexNames.size() == 1 && indexNames.contains(".security-7"))) |
| 2762 | + && (routingTableEntries.isEmpty() || (routingTableEntries.size() == 1 && routingTableEntries.contains(".security-7")))) { |
| 2763 | + checkSecurityIndex(); |
| 2764 | + } else { |
| 2765 | + // If there are any other indices or if this is for a non-default project, we fail the test. |
| 2766 | + assertThat("Project [" + projectId + "] should not have indices", indexNames, empty()); |
| 2767 | + assertThat("Project [" + projectId + "] should not have routing entries", routingTableEntries, empty()); |
| 2768 | + } |
| 2769 | + } |
| 2770 | + assertThat( |
| 2771 | + "Project [" + projectId + "] should not have graveyard entries", |
| 2772 | + state.evaluate("metadata.index-graveyard.tombstones"), |
| 2773 | + empty() |
| 2774 | + ); |
| 2775 | + |
| 2776 | + final Map<String, ?> legacyTemplates = state.evaluate("metadata.templates"); |
| 2777 | + if (legacyTemplates != null) { |
| 2778 | + var templateNames = legacyTemplates.keySet().stream().filter(name -> isXPackTemplate(name) == false).toList(); |
| 2779 | + assertThat("Project [" + projectId + "] should not have legacy templates", templateNames, empty()); |
| 2780 | + } |
| 2781 | + |
| 2782 | + final Map<String, Object> indexTemplates = state.evaluate("metadata.index_template.index_template"); |
| 2783 | + if (indexTemplates != null) { |
| 2784 | + var templateNames = indexTemplates.keySet().stream().filter(name -> isXPackTemplate(name) == false).toList(); |
| 2785 | + assertThat("Project [" + projectId + "] should not have index templates", templateNames, empty()); |
| 2786 | + } else if (projectId.equals(Metadata.DEFAULT_PROJECT_ID.id())) { |
| 2787 | + fail("Expected default project to have standard templates, but was null"); |
| 2788 | + } |
| 2789 | + |
| 2790 | + final Map<String, Object> componentTemplates = state.evaluate("metadata.component_template.component_template"); |
| 2791 | + if (componentTemplates != null) { |
| 2792 | + var templateNames = componentTemplates.keySet().stream().filter(name -> isXPackTemplate(name) == false).toList(); |
| 2793 | + assertThat("Project [" + projectId + "] should not have component templates", templateNames, empty()); |
| 2794 | + } else if (projectId.equals(Metadata.DEFAULT_PROJECT_ID.id())) { |
| 2795 | + fail("Expected default project to have standard component templates, but was null"); |
| 2796 | + } |
| 2797 | + |
| 2798 | + final List<Map<String, ?>> pipelines = state.evaluate("metadata.ingest.pipeline"); |
| 2799 | + if (pipelines != null) { |
| 2800 | + var pipelineNames = pipelines.stream() |
| 2801 | + .map(pipeline -> String.valueOf(pipeline.get("id"))) |
| 2802 | + .filter(id -> isXPackIngestPipeline(id) == false) |
| 2803 | + .toList(); |
| 2804 | + assertThat("Project [" + projectId + "] should not have ingest pipelines", pipelineNames, empty()); |
| 2805 | + } else if (projectId.equals(Metadata.DEFAULT_PROJECT_ID.id())) { |
| 2806 | + fail("Expected default project to have standard ingest pipelines, but was null"); |
| 2807 | + } |
| 2808 | + |
| 2809 | + if (has(ProductFeature.ILM)) { |
| 2810 | + final Map<String, Object> ilmPolicies = state.evaluate("metadata.index_lifecycle.policies"); |
| 2811 | + if (ilmPolicies != null) { |
| 2812 | + var policyNames = new HashSet<>(ilmPolicies.keySet()); |
| 2813 | + policyNames.removeAll(preserveILMPolicyIds()); |
| 2814 | + assertThat("Project [" + projectId + "] should not have ILM Policies", policyNames, empty()); |
| 2815 | + } else if (projectId.equals(Metadata.DEFAULT_PROJECT_ID.id())) { |
| 2816 | + fail("Expected default project to have standard ILM policies, but was null"); |
| 2817 | + } |
| 2818 | + } |
| 2819 | + } |
| 2820 | + |
| 2821 | + private void checkSecurityIndex() throws IOException { |
| 2822 | + assert multiProjectEnabled; |
| 2823 | + final Request request = new Request("GET", "/_security/_query/role"); |
| 2824 | + request.setJsonEntity(""" |
| 2825 | + { |
| 2826 | + "query": { |
| 2827 | + "bool": { |
| 2828 | + "must_not": { |
| 2829 | + "term": { |
| 2830 | + "metadata._reserved": true |
| 2831 | + } |
| 2832 | + } |
| 2833 | + } |
| 2834 | + } |
| 2835 | + }"""); |
| 2836 | + request.setOptions( |
| 2837 | + request.getOptions().toBuilder().addHeader(Task.X_ELASTIC_PROJECT_ID_HTTP_HEADER, Metadata.DEFAULT_PROJECT_ID.id()).build() |
| 2838 | + ); |
| 2839 | + final var response = responseAsMap(adminClient().performRequest(request)); |
| 2840 | + assertThat("Security index should not contain any non-reserved roles", (Collection<?>) response.get("roles"), empty()); |
| 2841 | + } |
2705 | 2842 | } |
0 commit comments