-
Notifications
You must be signed in to change notification settings - Fork 24.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HLRest: model role and privileges #35128
Changes from 30 commits
706b736
21bc280
9e174eb
64a91e2
d98232b
aabbc5f
9883726
ba34f65
e03a05b
a7b176b
701f85e
523f108
ee37904
36f7be5
395e29c
4604127
beaa373
05ccbd0
4c339e9
9b75cdb
4839eaa
192295a
d83113d
1d9f874
d4170f2
9869124
3ead50c
b68a3cf
769a0e1
24351b4
36d3203
20f14f7
59014c6
7fd629a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ApplicationResourcePrivileges, Void> 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<String> privileges = (Collection<String>) constructorObjects[i++]; | ||
final Collection<String> resources = (Collection<String>) 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<String> privileges; | ||
private final Set<String> 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<String> privileges, Collection<String> 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<String> getResources() { | ||
return this.resources; | ||
} | ||
|
||
public Set<String> 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); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
/* | ||
* 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 by operations. The privilege definition, as well as the operation | ||
* identifier, are outside of the Elasticsearch jurisdiction. Categories are | ||
* predefined and enforced by Elasticsearch. It is not permitted to define | ||
* different privileges for the same category and operation. | ||
*/ | ||
public class GlobalOperationPrivilege { | ||
|
||
private final String category; | ||
private final String operation; | ||
private final Map<String, Object> privilege; | ||
|
||
/** | ||
* Constructs privileges under a certain {@code category} and for some | ||
* {@code operation}. There is no constraint over the {@code operation} | ||
* identifier, only the categories are predefined. The privilege definition is | ||
* also out of Elasticsearch's control. | ||
* | ||
* @param category | ||
* The category of the privilege. | ||
* @param operation | ||
* The operation of the privilege. | ||
* @param privilege | ||
* The privilege definition. This is out of the Elasticsearch's | ||
* control. | ||
*/ | ||
public GlobalOperationPrivilege(String category, String operation, Map<String, Object> 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<String, Object> 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); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
/* | ||
* 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not true.
Instructs Elasticsearch that this role is allowed to manage application privileges under the application names "kibana-*". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was wrong, I got carried astray by too much abstraction.
|
||
* 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<String> CATEGORIES = Collections.unmodifiableList(Arrays.asList("application")); | ||
|
||
@SuppressWarnings("unchecked") | ||
static final ConstructingObjectParser<GlobalPrivileges, Void> 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<GlobalOperationPrivilege>) 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<? extends GlobalOperationPrivilege> privileges; | ||
|
||
/** | ||
* 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<? extends GlobalOperationPrivilege> 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))); | ||
final Map<String, List<GlobalOperationPrivilege>> privilegesByCategoryMap = | ||
this.privileges.stream().collect(Collectors.groupingBy(GlobalOperationPrivilege::getCategory)); | ||
for (final Map.Entry<String, List<GlobalOperationPrivilege>> privilegesByCategory : privilegesByCategoryMap.entrySet()) { | ||
// all operations for a specific category | ||
final Set<String> 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 { | ||
final Map<String, List<GlobalOperationPrivilege>> privilegesByCategoryMap = | ||
this.privileges.stream().collect(Collectors.groupingBy(GlobalOperationPrivilege::getCategory)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we just keep this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I went with your suggestion. At first, I have resisted duplicating information, but since it's immutable and private, I suppose it's OK. |
||
builder.startObject(); | ||
for (final Map.Entry<String, List<GlobalOperationPrivilege>> privilegesByCategory : 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<? extends GlobalOperationPrivilege> 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); | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't quite follow (or perhaps don't agree) with your point about the definition being "outside of the Elasticsearch jurisdiction". Elasticsearch definitely controls & evaluates these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was plainly wrong in my understanding.
This is now: