diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/ApplicationResourcePrivileges.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/ApplicationResourcePrivileges.java new file mode 100644 index 00000000000..8846e259e26 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/ApplicationResourcePrivileges.java @@ -0,0 +1,156 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.client.security.user.privileges; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; + +/** + * Represents privileges over resources that are scoped under an application. + * The application, resources and privileges are completely managed by the + * client and can be arbitrary string identifiers. Elasticsearch is not + * concerned by any resources under an application scope. + */ +public final class ApplicationResourcePrivileges implements ToXContentObject { + + private static final ParseField APPLICATION = new ParseField("application"); + private static final ParseField PRIVILEGES = new ParseField("privileges"); + private static final ParseField RESOURCES = new ParseField("resources"); + + @SuppressWarnings("unchecked") + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "application_privileges", false, constructorObjects -> { + // Don't ignore unknown fields. It is dangerous if the object we parse is also + // part of a request that we build later on, and the fields that we now ignore will + // end up being implicitly set to null in that request. + int i = 0; + final String application = (String) constructorObjects[i++]; + final Collection privileges = (Collection) constructorObjects[i++]; + final Collection resources = (Collection) constructorObjects[i]; + return new ApplicationResourcePrivileges(application, privileges, resources); + }); + + static { + PARSER.declareString(constructorArg(), APPLICATION); + PARSER.declareStringArray(constructorArg(), PRIVILEGES); + PARSER.declareStringArray(constructorArg(), RESOURCES); + } + + private final String application; + private final Set privileges; + private final Set resources; + + /** + * Constructs privileges for resources under an application scope. + * + * @param application + * The application name. This identifier is completely under the + * clients control. + * @param privileges + * The privileges names. Cannot be null or empty. Privilege + * identifiers are completely under the clients control. + * @param resources + * The resources names. Cannot be null or empty. Resource identifiers + * are completely under the clients control. + */ + public ApplicationResourcePrivileges(String application, Collection privileges, Collection resources) { + if (Strings.isNullOrEmpty(application)) { + throw new IllegalArgumentException("application privileges must have an application name"); + } + if (null == privileges || privileges.isEmpty()) { + throw new IllegalArgumentException("application privileges must define at least one privilege"); + } + if (null == resources || resources.isEmpty()) { + throw new IllegalArgumentException("application privileges must refer to at least one resource"); + } + this.application = application; + this.privileges = Collections.unmodifiableSet(new HashSet<>(privileges)); + this.resources = Collections.unmodifiableSet(new HashSet<>(resources)); + } + + public String getApplication() { + return application; + } + + public Set getResources() { + return this.resources; + } + + public Set getPrivileges() { + return this.privileges; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || this.getClass() != o.getClass()) { + return false; + } + ApplicationResourcePrivileges that = (ApplicationResourcePrivileges) o; + return application.equals(that.application) + && privileges.equals(that.privileges) + && resources.equals(that.resources); + } + + @Override + public int hashCode() { + return Objects.hash(application, privileges, resources); + } + + @Override + public String toString() { + try { + return XContentHelper.toXContent(this, XContentType.JSON, true).utf8ToString(); + } catch (IOException e) { + throw new RuntimeException("Unexpected", e); + } + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(APPLICATION.getPreferredName(), application); + builder.field(PRIVILEGES.getPreferredName(), privileges); + builder.field(RESOURCES.getPreferredName(), resources); + return builder.endObject(); + } + + public static ApplicationResourcePrivileges fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + +} \ No newline at end of file diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/GlobalOperationPrivilege.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/GlobalOperationPrivilege.java new file mode 100644 index 00000000000..507d6a5a195 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/GlobalOperationPrivilege.java @@ -0,0 +1,99 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.client.security.user.privileges; + +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; + +/** + * Represents generic global cluster privileges that can be scoped by categories + * and then further by operations. The privilege's syntactic and semantic + * meaning is specific to each category and operation; there is no general + * definition template. It is not permitted to define different privileges under + * the same category and operation. + */ +public class GlobalOperationPrivilege { + + private final String category; + private final String operation; + private final Map privilege; + + /** + * Constructs privileges under a specific {@code category} and for some + * {@code operation}. The privilege definition is flexible, it is a {@code Map}, + * and the semantics is bound to the {@code category} and {@code operation}. + * + * @param category + * The category of the privilege. + * @param operation + * The operation of the privilege. + * @param privilege + * The privilege definition. + */ + public GlobalOperationPrivilege(String category, String operation, Map privilege) { + this.category = Objects.requireNonNull(category); + this.operation = Objects.requireNonNull(operation); + if (privilege == null || privilege.isEmpty()) { + throw new IllegalArgumentException("Privileges cannot be empty or null"); + } + this.privilege = Collections.unmodifiableMap(privilege); + } + + public String getCategory() { + return category; + } + + public String getOperation() { + return operation; + } + + public Map getRaw() { + return privilege; + } + + public static GlobalOperationPrivilege fromXContent(String category, String operation, XContentParser parser) throws IOException { + // parser is still placed on the field name, advance to next token (field value) + assert parser.currentToken().equals(XContentParser.Token.FIELD_NAME); + parser.nextToken(); + return new GlobalOperationPrivilege(category, operation, parser.map()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || (false == this instanceof GlobalOperationPrivilege)) { + return false; + } + final GlobalOperationPrivilege that = (GlobalOperationPrivilege) o; + return category.equals(that.category) && operation.equals(that.operation) && privilege.equals(that.privilege); + } + + @Override + public int hashCode() { + return Objects.hash(category, operation, privilege); + } + +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/GlobalPrivileges.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/GlobalPrivileges.java new file mode 100644 index 00000000000..89198076542 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/GlobalPrivileges.java @@ -0,0 +1,137 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.client.security.user.privileges; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * Represents global privileges. "Global Privilege" is a mantra for granular + * generic cluster privileges. These privileges are organized into categories. + * Elasticsearch defines the set of categories. Under each category there are + * operations that are under the clients jurisdiction. The privilege is hence + * defined under an operation under a category. + */ +public final class GlobalPrivileges implements ToXContentObject { + + // When categories change, adapting this field should suffice. Categories are NOT + // opaque "named_objects", we wish to maintain control over these namespaces + static final List CATEGORIES = Collections.unmodifiableList(Arrays.asList("application")); + + @SuppressWarnings("unchecked") + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("global_category_privileges", + false, constructorObjects -> { + // ignore_unknown_fields is irrelevant here anyway, but let's keep it to false + // because this conveys strictness (woop woop) + return new GlobalPrivileges((Collection) constructorObjects[0]); + }); + + static { + for (final String category : CATEGORIES) { + PARSER.declareNamedObjects(optionalConstructorArg(), + (parser, context, operation) -> GlobalOperationPrivilege.fromXContent(category, operation, parser), + new ParseField(category)); + } + } + + private final Set privileges; + // same data as in privileges but broken down by categories; internally, it is + // easier to work with this structure + private final Map> privilegesByCategoryMap; + + /** + * Constructs global privileges by bundling the set of privileges. + * + * @param privileges + * The privileges under a category and for an operation in that category. + */ + public GlobalPrivileges(Collection privileges) { + if (privileges == null || privileges.isEmpty()) { + throw new IllegalArgumentException("Privileges cannot be empty or null"); + } + // duplicates are just ignored + this.privileges = Collections.unmodifiableSet(new HashSet<>(Objects.requireNonNull(privileges))); + this.privilegesByCategoryMap = Collections + .unmodifiableMap(this.privileges.stream().collect(Collectors.groupingBy(GlobalOperationPrivilege::getCategory))); + for (final Map.Entry> privilegesByCategory : privilegesByCategoryMap.entrySet()) { + // all operations for a specific category + final Set allOperations = privilegesByCategory.getValue().stream().map(p -> p.getOperation()) + .collect(Collectors.toSet()); + if (allOperations.size() != privilegesByCategory.getValue().size()) { + throw new IllegalArgumentException("Different privileges for the same category and operation are not permitted"); + } + } + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + for (final Map.Entry> privilegesByCategory : this.privilegesByCategoryMap.entrySet()) { + builder.startObject(privilegesByCategory.getKey()); + for (final GlobalOperationPrivilege privilege : privilegesByCategory.getValue()) { + builder.field(privilege.getOperation(), privilege.getRaw()); + } + builder.endObject(); + } + return builder.endObject(); + } + + public static GlobalPrivileges fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + + public Set getPrivileges() { + return privileges; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || this.getClass() != o.getClass()) { + return false; + } + final GlobalPrivileges that = (GlobalPrivileges) o; + return privileges.equals(that.privileges); + } + + @Override + public int hashCode() { + return Objects.hash(privileges); + } + +} \ No newline at end of file diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/IndicesPrivileges.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/IndicesPrivileges.java new file mode 100644 index 00000000000..e693a4fea34 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/IndicesPrivileges.java @@ -0,0 +1,309 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.client.security.user.privileges; + +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * Represents privileges over indices. There is a canonical set of privilege + * names (eg. {@code IndicesPrivileges#READ_PRIVILEGE_NAME}) but there is + * flexibility in the definition of finer grained, more specialized, privileges. + * This also encapsulates field and document level security privileges. These + * allow to control what fields or documents are readable or queryable. + */ +public final class IndicesPrivileges implements ToXContentObject { + + public static final ParseField NAMES = new ParseField("names"); + public static final ParseField PRIVILEGES = new ParseField("privileges"); + public static final ParseField FIELD_PERMISSIONS = new ParseField("field_security"); + public static final ParseField GRANT_FIELDS = new ParseField("grant"); + public static final ParseField EXCEPT_FIELDS = new ParseField("except"); + public static final ParseField QUERY = new ParseField("query"); + + @SuppressWarnings("unchecked") + static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("indices_privileges", false, constructorObjects -> { + int i = 0; + final Collection indices = (Collection) constructorObjects[i++]; + final Collection privileges = (Collection) constructorObjects[i++]; + final Tuple, Collection> fields = + (Tuple, Collection>) constructorObjects[i++]; + final Collection grantFields = fields != null ? fields.v1() : null; + final Collection exceptFields = fields != null ? fields.v2() : null; + final String query = (String) constructorObjects[i]; + return new IndicesPrivileges(indices, privileges, grantFields, exceptFields, query); + }); + + static { + @SuppressWarnings("unchecked") + final ConstructingObjectParser, Collection>, Void> fls_parser = + new ConstructingObjectParser<>( "field_level_parser", false, constructorObjects -> { + int i = 0; + final Collection grantFields = (Collection) constructorObjects[i++]; + final Collection exceptFields = (Collection) constructorObjects[i]; + return new Tuple<>(grantFields, exceptFields); + }); + fls_parser.declareStringArray(optionalConstructorArg(), GRANT_FIELDS); + fls_parser.declareStringArray(optionalConstructorArg(), EXCEPT_FIELDS); + + PARSER.declareStringArray(constructorArg(), NAMES); + PARSER.declareStringArray(constructorArg(), PRIVILEGES); + PARSER.declareObject(optionalConstructorArg(), fls_parser, FIELD_PERMISSIONS); + PARSER.declareStringOrNull(optionalConstructorArg(), QUERY); + } + + private final Set indices; + private final Set privileges; + // null or singleton '*' means all fields are granted, empty means no fields are granted + private final @Nullable Set grantedFields; + // null or empty means no fields are denied + private final @Nullable Set deniedFields; + // missing query means all documents, i.e. no restrictions + private final @Nullable String query; + + private IndicesPrivileges(Collection indices, Collection privileges, @Nullable Collection grantedFields, + @Nullable Collection deniedFields, @Nullable String query) { + if (null == indices || indices.isEmpty()) { + throw new IllegalArgumentException("indices privileges must refer to at least one index name or index name pattern"); + } + if (null == privileges || privileges.isEmpty()) { + throw new IllegalArgumentException("indices privileges must define at least one privilege"); + } + this.indices = Collections.unmodifiableSet(new HashSet<>(indices)); + this.privileges = Collections.unmodifiableSet(new HashSet<>(privileges)); + // unspecified granted fields means no restriction + this.grantedFields = grantedFields == null ? null : Collections.unmodifiableSet(new HashSet<>(grantedFields)); + // unspecified denied fields means no restriction + this.deniedFields = deniedFields == null ? null : Collections.unmodifiableSet(new HashSet<>(deniedFields)); + this.query = query; + } + + /** + * The indices names covered by the privileges. + */ + public Set getIndices() { + return this.indices; + } + + /** + * The privileges acting over indices. There is a canonical predefined set of + * such privileges, but the {@code String} datatype allows for flexibility in defining + * finer grained privileges. + */ + public Set getPrivileges() { + return this.privileges; + } + + /** + * The document fields that can be read or queried. Can be null, in this case + * all the document's fields are granted access to. Can also be empty, in which + * case no fields are granted access to. + */ + public @Nullable Set getGrantedFields() { + return this.grantedFields; + } + + /** + * The document fields that cannot be accessed or queried. Can be null or empty, + * in which case no fields are denied. + */ + public @Nullable Set getDeniedFields() { + return this.deniedFields; + } + + /** + * A query limiting the visible documents in the indices. Can be null, in which + * case all documents are visible. + */ + public @Nullable String getQuery() { + return this.query; + } + + /** + * If {@code true} some documents might not be visible. Only the documents + * matching {@code query} will be readable. + */ + public boolean isUsingDocumentLevelSecurity() { + return query != null; + } + + /** + * If {@code true} some document fields might not be visible. + */ + public boolean isUsingFieldLevelSecurity() { + return limitsGrantedFields() || hasDeniedFields(); + } + + private boolean hasDeniedFields() { + return deniedFields != null && false == deniedFields.isEmpty(); + } + + private boolean limitsGrantedFields() { + // we treat just '*' as no FLS since that's what the UI defaults to + if (grantedFields == null || (grantedFields.size() == 1 && grantedFields.iterator().next().equals("*"))) { + return false; + } + return true; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + IndicesPrivileges that = (IndicesPrivileges) o; + return indices.equals(that.indices) + && privileges.equals(that.privileges) + && Objects.equals(grantedFields, that.grantedFields) + && Objects.equals(deniedFields, that.deniedFields) + && Objects.equals(query, that.query); + } + + @Override + public int hashCode() { + return Objects.hash(indices, privileges, grantedFields, deniedFields, query); + } + + @Override + public String toString() { + try { + return XContentHelper.toXContent(this, XContentType.JSON, true).utf8ToString(); + } catch (IOException e) { + throw new RuntimeException("Unexpected", e); + } + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(NAMES.getPreferredName(), indices); + builder.field(PRIVILEGES.getPreferredName(), privileges); + if (isUsingFieldLevelSecurity()) { + builder.startObject(FIELD_PERMISSIONS.getPreferredName()); + if (grantedFields != null) { + builder.field(GRANT_FIELDS.getPreferredName(), grantedFields); + } + if (hasDeniedFields()) { + builder.field(EXCEPT_FIELDS.getPreferredName(), deniedFields); + } + builder.endObject(); + } + if (isUsingDocumentLevelSecurity()) { + builder.field("query", query); + } + return builder.endObject(); + } + + public static IndicesPrivileges fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + + private @Nullable Collection indices = null; + private @Nullable Collection privileges = null; + private @Nullable Collection grantedFields = null; + private @Nullable Collection deniedFields = null; + private @Nullable String query = null; + + private Builder() { + } + + public Builder indices(String... indices) { + return indices(Arrays.asList(Objects.requireNonNull(indices, "indices required"))); + } + + public Builder indices(Collection indices) { + this.indices = Objects.requireNonNull(indices, "indices required"); + return this; + } + + public Builder privileges(String... privileges) { + return privileges(Arrays.asList(Objects.requireNonNull(privileges, "privileges required"))); + } + + public Builder privileges(Collection privileges) { + this.privileges = Objects.requireNonNull(privileges, "privileges required"); + return this; + } + + public Builder grantedFields(@Nullable String... grantedFields) { + if (grantedFields == null) { + this.grantedFields = null; + return this; + } + return grantedFields(Arrays.asList(grantedFields)); + } + + public Builder grantedFields(@Nullable Collection grantedFields) { + this.grantedFields = grantedFields; + return this; + } + + public Builder deniedFields(@Nullable String... deniedFields) { + if (deniedFields == null) { + this.deniedFields = null; + return this; + } + return deniedFields(Arrays.asList(deniedFields)); + } + + public Builder deniedFields(@Nullable Collection deniedFields) { + this.deniedFields = deniedFields; + return this; + } + + public Builder query(@Nullable String query) { + this.query = query; + return this; + } + + public IndicesPrivileges build() { + return new IndicesPrivileges(indices, privileges, grantedFields, deniedFields, query); + } + } + +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/ManageApplicationPrivilege.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/ManageApplicationPrivilege.java new file mode 100644 index 00000000000..9356c2ef0e8 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/ManageApplicationPrivilege.java @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.client.security.user.privileges; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Represents the privilege to "manage" certain applications. The "manage" + * privilege is actually defined outside of Elasticsearch. + */ +public class ManageApplicationPrivilege extends GlobalOperationPrivilege { + + private static final String CATEGORY = "application"; + private static final String OPERATION = "manage"; + private static final String KEY = "applications"; + + public ManageApplicationPrivilege(Collection applications) { + super(CATEGORY, OPERATION, Collections.singletonMap(KEY, new HashSet(Objects.requireNonNull(applications)))); + } + + @SuppressWarnings("unchecked") + public Set getManagedApplications() { + return (Set)getRaw().get(KEY); + } + + @Override + public boolean equals(Object o) { + return super.equals(o); + } + + @Override + public int hashCode() { + return super.hashCode(); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/Role.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/Role.java new file mode 100644 index 00000000000..78265196ee8 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/Role.java @@ -0,0 +1,310 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.client.security.user.privileges; + +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.ObjectParser.ValueType; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg; + +/** + * Represents an aggregation of privileges. This does not have a name + * identifier. + */ +public final class Role implements ToXContentObject { + + public static final ParseField CLUSTER = new ParseField("cluster"); + public static final ParseField GLOBAL = new ParseField("global"); + public static final ParseField INDICES = new ParseField("indices"); + public static final ParseField APPLICATIONS = new ParseField("applications"); + public static final ParseField RUN_AS = new ParseField("run_as"); + public static final ParseField METADATA = new ParseField("metadata"); + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("role_descriptor", false, + constructorObjects -> { + // Don't ignore unknown fields. It is dangerous if the object we parse is also + // part of a request that we build later on, and the fields that we now ignore + // will end up being implicitly set to null in that request. + int i = 0; + final Collection clusterPrivileges = (Collection) constructorObjects[i++]; + final GlobalPrivileges globalApplicationPrivileges = (GlobalPrivileges) constructorObjects[i++]; + final Collection indicesPrivileges = (Collection) constructorObjects[i++]; + final Collection applicationResourcePrivileges = + (Collection) constructorObjects[i++]; + final Collection runAsPrivilege = (Collection) constructorObjects[i++]; + final Map metadata = (Map) constructorObjects[i]; + return new Role(clusterPrivileges, globalApplicationPrivileges, indicesPrivileges, applicationResourcePrivileges, + runAsPrivilege, metadata); + }); + + static { + PARSER.declareStringArray(optionalConstructorArg(), CLUSTER); + PARSER.declareObject(optionalConstructorArg(), GlobalPrivileges.PARSER, GLOBAL); + PARSER.declareFieldArray(optionalConstructorArg(), IndicesPrivileges.PARSER, INDICES, ValueType.OBJECT_ARRAY); + PARSER.declareFieldArray(optionalConstructorArg(), ApplicationResourcePrivileges.PARSER, APPLICATIONS, ValueType.OBJECT_ARRAY); + PARSER.declareStringArray(optionalConstructorArg(), RUN_AS); + PARSER.declareObject(constructorArg(), (parser, c) -> parser.map(), METADATA); + } + + private final Set clusterPrivileges; + private final @Nullable GlobalPrivileges globalApplicationPrivileges; + private final Set indicesPrivileges; + private final Set applicationResourcePrivileges; + private final Set runAsPrivilege; + private final Map metadata; + + private Role(@Nullable Collection clusterPrivileges, @Nullable GlobalPrivileges globalApplicationPrivileges, + @Nullable Collection indicesPrivileges, + @Nullable Collection applicationResourcePrivileges, @Nullable Collection runAsPrivilege, + @Nullable Map metadata) { + // no cluster privileges are granted unless otherwise specified + this.clusterPrivileges = Collections + .unmodifiableSet(clusterPrivileges != null ? new HashSet<>(clusterPrivileges) : Collections.emptySet()); + this.globalApplicationPrivileges = globalApplicationPrivileges; + // no indices privileges are granted unless otherwise specified + this.indicesPrivileges = Collections + .unmodifiableSet(indicesPrivileges != null ? new HashSet<>(indicesPrivileges) : Collections.emptySet()); + // no application resource privileges are granted unless otherwise specified + this.applicationResourcePrivileges = Collections.unmodifiableSet( + applicationResourcePrivileges != null ? new HashSet<>(applicationResourcePrivileges) : Collections.emptySet()); + // no run as privileges are granted unless otherwise specified + this.runAsPrivilege = Collections.unmodifiableSet(runAsPrivilege != null ? new HashSet<>(runAsPrivilege) : Collections.emptySet()); + this.metadata = metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap(); + } + + public Set getClusterPrivileges() { + return clusterPrivileges; + } + + public GlobalPrivileges getGlobalApplicationPrivileges() { + return globalApplicationPrivileges; + } + + public Set getIndicesPrivileges() { + return indicesPrivileges; + } + + public Set getApplicationResourcePrivileges() { + return applicationResourcePrivileges; + } + + public Set getRunAsPrivilege() { + return runAsPrivilege; + } + + public Map getMetadata() { + return metadata; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Role that = (Role) o; + return clusterPrivileges.equals(that.clusterPrivileges) + && Objects.equals(globalApplicationPrivileges, that.globalApplicationPrivileges) + && indicesPrivileges.equals(that.indicesPrivileges) + && applicationResourcePrivileges.equals(that.applicationResourcePrivileges) + && runAsPrivilege.equals(that.runAsPrivilege) + && metadata.equals(that.metadata); + } + + @Override + public int hashCode() { + return Objects.hash(clusterPrivileges, globalApplicationPrivileges, indicesPrivileges, applicationResourcePrivileges, + runAsPrivilege, metadata); + } + + @Override + public String toString() { + try { + return XContentHelper.toXContent(this, XContentType.JSON, true).utf8ToString(); + } catch (IOException e) { + throw new RuntimeException("Unexpected", e); + } + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (false == clusterPrivileges.isEmpty()) { + builder.field(CLUSTER.getPreferredName(), clusterPrivileges); + } + if (null != globalApplicationPrivileges) { + builder.field(GLOBAL.getPreferredName(), globalApplicationPrivileges); + } + if (false == indicesPrivileges.isEmpty()) { + builder.field(INDICES.getPreferredName(), indicesPrivileges); + } + if (false == applicationResourcePrivileges.isEmpty()) { + builder.field(APPLICATIONS.getPreferredName(), applicationResourcePrivileges); + } + if (false == runAsPrivilege.isEmpty()) { + builder.field(RUN_AS.getPreferredName(), runAsPrivilege); + } + if (false == metadata.isEmpty()) { + builder.field(METADATA.getPreferredName(), metadata); + } + return builder.endObject(); + } + + public static Role fromXContent(XContentParser parser) { + return PARSER.apply(parser, null); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + + private @Nullable Collection clusterPrivileges = null; + private @Nullable GlobalPrivileges globalApplicationPrivileges = null; + private @Nullable Collection indicesPrivileges = null; + private @Nullable Collection applicationResourcePrivileges = null; + private @Nullable Collection runAsPrivilege = null; + private @Nullable Map metadata = null; + + private Builder() { + } + + public Builder clusterPrivileges(String... clusterPrivileges) { + return clusterPrivileges(Arrays + .asList(Objects.requireNonNull(clusterPrivileges, "Cluster privileges cannot be null. Pass an empty array instead."))); + } + + public Builder clusterPrivileges(Collection clusterPrivileges) { + this.clusterPrivileges = Objects.requireNonNull(clusterPrivileges, + "Cluster privileges cannot be null. Pass an empty collection instead."); + return this; + } + + public Builder glabalApplicationPrivileges(GlobalPrivileges globalApplicationPrivileges) { + this.globalApplicationPrivileges = globalApplicationPrivileges; + return this; + } + + public Builder indicesPrivileges(IndicesPrivileges... indicesPrivileges) { + return indicesPrivileges(Arrays + .asList(Objects.requireNonNull(indicesPrivileges, "Indices privileges cannot be null. Pass an empty array instead."))); + } + + public Builder indicesPrivileges(Collection indicesPrivileges) { + this.indicesPrivileges = Objects.requireNonNull(indicesPrivileges, + "Indices privileges cannot be null. Pass an empty collection instead."); + return this; + } + + public Builder applicationResourcePrivileges(ApplicationResourcePrivileges... applicationResourcePrivileges) { + return applicationResourcePrivileges(Arrays.asList(Objects.requireNonNull(applicationResourcePrivileges, + "Application resource privileges cannot be null. Pass an empty array instead."))); + } + + public Builder applicationResourcePrivileges(Collection applicationResourcePrivileges) { + this.applicationResourcePrivileges = Objects.requireNonNull(applicationResourcePrivileges, + "Application resource privileges cannot be null. Pass an empty collection instead."); + return this; + } + + public Builder runAsPrivilege(String... runAsPrivilege) { + return runAsPrivilege(Arrays + .asList(Objects.requireNonNull(runAsPrivilege, "Run as privilege cannot be null. Pass an empty array instead."))); + } + + public Builder runAsPrivilege(Collection runAsPrivilege) { + this.runAsPrivilege = Objects.requireNonNull(runAsPrivilege, + "Run as privilege cannot be null. Pass an empty collection instead."); + return this; + } + + public Builder metadata(Map metadata) { + this.metadata = Objects.requireNonNull(metadata, "Metadata cannot be null. Pass an empty map instead."); + return this; + } + + public Role build() { + return new Role(clusterPrivileges, globalApplicationPrivileges, indicesPrivileges, applicationResourcePrivileges, + runAsPrivilege, metadata); + } + } + + /** + * Canonical cluster privilege names. There is no enforcement to only use these. + */ + public static class ClusterPrivilegeName { + public static final String NONE = "none"; + public static final String ALL = "all"; + public static final String MONITOR = "monitor"; + public static final String MONITOR_ML = "monitor_ml"; + public static final String MONITOR_WATCHER = "monitor_watcher"; + public static final String MONITOR_ROLLUP = "monitor_rollup"; + public static final String MANAGE = "manage"; + public static final String MANAGE_ML = "manage_ml"; + public static final String MANAGE_WATCHER = "manage_watcher"; + public static final String MANAGE_ROLLUP = "manage_rollup"; + public static final String MANAGE_INDEX_TEMPLATES = "manage_index_templates"; + public static final String MANAGE_INGEST_PIPELINES = "manage_ingest_pipelines"; + public static final String TRANSPORT_CLIENT = "transport_client"; + public static final String MANAGE_SECURITY = "manage_security"; + public static final String MANAGE_SAML = "manage_saml"; + public static final String MANAGE_PIPELINE = "manage_pipeline"; + public static final String MANAGE_CCR = "manage_ccr"; + public static final String READ_CCR = "read_ccr"; + } + + /** + * Canonical index privilege names. There is no enforcement to only use these. + */ + public static class IndexPrivilegeName { + public static final String NONE = "none"; + public static final String ALL = "all"; + public static final String READ = "read"; + public static final String READ_CROSS = "read_cross_cluster"; + public static final String CREATE = "create"; + public static final String INDEX = "index"; + public static final String DELETE = "delete"; + public static final String WRITE = "write"; + public static final String MONITOR = "monitor"; + public static final String MANAGE = "manage"; + public static final String DELETE_INDEX = "delete_index"; + public static final String CREATE_INDEX = "create_index"; + public static final String VIEW_INDEX_METADATA = "view_index_metadata"; + public static final String MANAGE_FOLLOW_INDEX = "manage_follow_index"; + } + +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/user/privileges/ApplicationResourcePrivilegesTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/user/privileges/ApplicationResourcePrivilegesTests.java new file mode 100644 index 00000000000..9575363a409 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/user/privileges/ApplicationResourcePrivilegesTests.java @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.client.security.user.privileges; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import static org.hamcrest.Matchers.is; + +public class ApplicationResourcePrivilegesTests extends AbstractXContentTestCase { + + @Override + protected ApplicationResourcePrivileges createTestInstance() { + return new ApplicationResourcePrivileges(randomAlphaOfLengthBetween(1, 8), + Arrays.asList(randomArray(1, 8, size -> new String[size], () -> randomAlphaOfLengthBetween(1, 8))), + Arrays.asList(randomArray(1, 8, size -> new String[size], () -> randomAlphaOfLengthBetween(1, 8)))); + } + + @Override + protected ApplicationResourcePrivileges doParseInstance(XContentParser parser) throws IOException { + return ApplicationResourcePrivileges.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } + + public void testEmptyApplicationName() { + final String emptyApplicationName = randomBoolean() ? "" : null; + final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> new ApplicationResourcePrivileges(emptyApplicationName, + Arrays.asList(randomArray(1, 8, size -> new String[size], () -> randomAlphaOfLengthBetween(1, 8))), + Arrays.asList(randomArray(1, 8, size -> new String[size], () -> randomAlphaOfLengthBetween(1, 8))))); + assertThat(e.getMessage(), is("application privileges must have an application name")); + } + + public void testEmptyPrivileges() { + final Collection emptyPrivileges = randomBoolean() ? Collections.emptyList() : null; + final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> new ApplicationResourcePrivileges(randomAlphaOfLengthBetween(1, 8), + emptyPrivileges, + Arrays.asList(randomArray(1, 8, size -> new String[size], () -> randomAlphaOfLengthBetween(1, 8))))); + assertThat(e.getMessage(), is("application privileges must define at least one privilege")); + } + + public void testEmptyResources() { + final Collection emptyResources = randomBoolean() ? Collections.emptyList() : null; + final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> new ApplicationResourcePrivileges(randomAlphaOfLengthBetween(1, 8), + Arrays.asList(randomArray(1, 8, size -> new String[size], () -> randomAlphaOfLengthBetween(1, 8))), + emptyResources)); + assertThat(e.getMessage(), is("application privileges must refer to at least one resource")); + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/user/privileges/GlobalPrivilegesTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/user/privileges/GlobalPrivilegesTests.java new file mode 100644 index 00000000000..bb1e9330891 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/user/privileges/GlobalPrivilegesTests.java @@ -0,0 +1,94 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.client.security.user.privileges; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.is; + +public class GlobalPrivilegesTests extends AbstractXContentTestCase { + + private static long idCounter = 0; + + @Override + protected GlobalPrivileges createTestInstance() { + final List privilegeList = Arrays + .asList(randomArray(1, 4, size -> new GlobalOperationPrivilege[size], () -> buildRandomGlobalScopedPrivilege())); + return new GlobalPrivileges(privilegeList); + } + + @Override + protected GlobalPrivileges doParseInstance(XContentParser parser) throws IOException { + return GlobalPrivileges.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return false; // true really means inserting bogus privileges + } + + public void testEmptyOrNullGlobalOperationPrivilege() { + final Map privilege = randomBoolean() ? null : Collections.emptyMap(); + final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> new GlobalOperationPrivilege(randomAlphaOfLength(2), randomAlphaOfLength(2), privilege)); + assertThat(e.getMessage(), is("Privileges cannot be empty or null")); + } + + public void testEmptyOrNullGlobalPrivileges() { + final List privileges = randomBoolean() ? null : Collections.emptyList(); + final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> new GlobalPrivileges(privileges)); + assertThat(e.getMessage(), is("Privileges cannot be empty or null")); + } + + public void testDuplicateGlobalOperationPrivilege() { + final GlobalOperationPrivilege privilege = buildRandomGlobalScopedPrivilege(); + // duplicate + final GlobalOperationPrivilege privilege2 = new GlobalOperationPrivilege(privilege.getCategory(), privilege.getOperation(), + new HashMap<>(privilege.getRaw())); + final GlobalPrivileges globalPrivilege = new GlobalPrivileges(Arrays.asList(privilege, privilege2)); + assertThat(globalPrivilege.getPrivileges().size(), is(1)); + assertThat(globalPrivilege.getPrivileges().iterator().next(), is(privilege)); + } + + public void testSameScopeGlobalOperationPrivilege() { + final GlobalOperationPrivilege privilege = buildRandomGlobalScopedPrivilege(); + final GlobalOperationPrivilege sameOperationPrivilege = new GlobalOperationPrivilege(privilege.getCategory(), + privilege.getOperation(), buildRandomGlobalScopedPrivilege().getRaw()); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> new GlobalPrivileges(Arrays.asList(privilege, sameOperationPrivilege))); + assertThat(e.getMessage(), is("Different privileges for the same category and operation are not permitted")); + } + + private static GlobalOperationPrivilege buildRandomGlobalScopedPrivilege() { + final Map privilege = new HashMap<>(); + for (int i = 0; i < randomIntBetween(1, 4); i++) { + privilege.put(randomAlphaOfLength(2) + idCounter++, randomAlphaOfLengthBetween(1, 4)); + } + return new GlobalOperationPrivilege("application", randomAlphaOfLength(2) + idCounter++, privilege); + } +}