|
@@ -14,7 +14,7 @@ import org.elasticsearch.transport.TransportRequest;
|
|
|
import org.elasticsearch.xpack.core.security.authc.Authentication;
|
|
|
import org.elasticsearch.xpack.core.security.authc.AuthenticationField;
|
|
|
import org.elasticsearch.xpack.core.security.authc.Subject;
|
|
|
-import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.AuthorizationInfo;
|
|
|
+import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
|
|
|
import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver;
|
|
|
import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege;
|
|
|
|
|
@@ -29,158 +29,202 @@ import static org.elasticsearch.common.Strings.collectionToCommaDelimitedString;
|
|
|
import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME;
|
|
|
import static org.elasticsearch.xpack.security.authz.AuthorizationService.isIndexAction;
|
|
|
|
|
|
-class AuthorizationDenialMessages {
|
|
|
-
|
|
|
- private AuthorizationDenialMessages() {}
|
|
|
-
|
|
|
- static String runAsDenied(Authentication authentication, @Nullable AuthorizationInfo authorizationInfo, String action) {
|
|
|
- assert authentication.isRunAs() : "constructing run as denied message but authentication for action was not run as";
|
|
|
-
|
|
|
- String userText = authenticatedUserDescription(authentication);
|
|
|
- String actionIsUnauthorizedMessage = actionIsUnauthorizedMessage(action, userText);
|
|
|
-
|
|
|
- String unauthorizedToRunAsMessage = "because "
|
|
|
- + userText
|
|
|
- + " is unauthorized to run as ["
|
|
|
- + authentication.getEffectiveSubject().getUser().principal()
|
|
|
- + "]";
|
|
|
-
|
|
|
- return actionIsUnauthorizedMessage
|
|
|
- + rolesDescription(authentication.getAuthenticatingSubject(), authorizationInfo.getAuthenticatedUserAuthorizationInfo())
|
|
|
- + ", "
|
|
|
- + unauthorizedToRunAsMessage;
|
|
|
- }
|
|
|
-
|
|
|
- static String actionDenied(
|
|
|
+public interface AuthorizationDenialMessages {
|
|
|
+ String actionDenied(
|
|
|
Authentication authentication,
|
|
|
- @Nullable AuthorizationInfo authorizationInfo,
|
|
|
+ @Nullable AuthorizationEngine.AuthorizationInfo authorizationInfo,
|
|
|
String action,
|
|
|
TransportRequest request,
|
|
|
@Nullable String context
|
|
|
- ) {
|
|
|
- String userText = successfulAuthenticationDescription(authentication, authorizationInfo);
|
|
|
- String remoteClusterText = authentication.isCrossClusterAccess() ? remoteClusterText(null) : "";
|
|
|
- String message = actionIsUnauthorizedMessage(action, remoteClusterText, userText);
|
|
|
- if (context != null) {
|
|
|
- message = message + " " + context;
|
|
|
+ );
|
|
|
+
|
|
|
+ String runAsDenied(Authentication authentication, @Nullable AuthorizationEngine.AuthorizationInfo authorizationInfo, String action);
|
|
|
+
|
|
|
+ String remoteActionDenied(
|
|
|
+ Authentication authentication,
|
|
|
+ @Nullable AuthorizationEngine.AuthorizationInfo authorizationInfo,
|
|
|
+ String action,
|
|
|
+ String clusterAlias
|
|
|
+ );
|
|
|
+
|
|
|
+ class Default implements AuthorizationDenialMessages {
|
|
|
+ public Default() {}
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String runAsDenied(
|
|
|
+ Authentication authentication,
|
|
|
+ @Nullable AuthorizationEngine.AuthorizationInfo authorizationInfo,
|
|
|
+ String action
|
|
|
+ ) {
|
|
|
+ assert authentication.isRunAs() : "constructing run as denied message but authentication for action was not run as";
|
|
|
+
|
|
|
+ String userText = authenticatedUserDescription(authentication);
|
|
|
+ String actionIsUnauthorizedMessage = actionIsUnauthorizedMessage(action, userText);
|
|
|
+
|
|
|
+ String unauthorizedToRunAsMessage = "because "
|
|
|
+ + userText
|
|
|
+ + " is unauthorized to run as ["
|
|
|
+ + authentication.getEffectiveSubject().getUser().principal()
|
|
|
+ + "]";
|
|
|
+
|
|
|
+ return actionIsUnauthorizedMessage
|
|
|
+ + rolesDescription(authentication.getAuthenticatingSubject(), authorizationInfo.getAuthenticatedUserAuthorizationInfo())
|
|
|
+ + ", "
|
|
|
+ + unauthorizedToRunAsMessage;
|
|
|
}
|
|
|
|
|
|
- if (ClusterPrivilegeResolver.isClusterAction(action)) {
|
|
|
- final Collection<String> privileges = ClusterPrivilegeResolver.findPrivilegesThatGrant(action, request, authentication);
|
|
|
- if (privileges != null && privileges.size() > 0) {
|
|
|
- message = message
|
|
|
- + ", this action is granted by the cluster privileges ["
|
|
|
- + collectionToCommaDelimitedString(privileges)
|
|
|
- + "]";
|
|
|
+ @Override
|
|
|
+ public String actionDenied(
|
|
|
+ Authentication authentication,
|
|
|
+ @Nullable AuthorizationEngine.AuthorizationInfo authorizationInfo,
|
|
|
+ String action,
|
|
|
+ TransportRequest request,
|
|
|
+ @Nullable String context
|
|
|
+ ) {
|
|
|
+ String userText = successfulAuthenticationDescription(authentication, authorizationInfo);
|
|
|
+ String remoteClusterText = authentication.isCrossClusterAccess() ? remoteClusterText(null) : "";
|
|
|
+ String message = actionIsUnauthorizedMessage(action, remoteClusterText, userText);
|
|
|
+ if (context != null) {
|
|
|
+ message = message + " " + context;
|
|
|
}
|
|
|
- } else if (isIndexAction(action)) {
|
|
|
- final Collection<String> privileges = IndexPrivilege.findPrivilegesThatGrant(action);
|
|
|
- if (privileges != null && privileges.size() > 0) {
|
|
|
- message = message
|
|
|
- + ", this action is granted by the index privileges ["
|
|
|
- + collectionToCommaDelimitedString(privileges)
|
|
|
- + "]";
|
|
|
+
|
|
|
+ if (ClusterPrivilegeResolver.isClusterAction(action)) {
|
|
|
+ final Collection<String> privileges = findClusterPrivilegesThatGrant(authentication, action, request);
|
|
|
+ if (privileges != null && privileges.size() > 0) {
|
|
|
+ message = message
|
|
|
+ + ", this action is granted by the cluster privileges ["
|
|
|
+ + collectionToCommaDelimitedString(privileges)
|
|
|
+ + "]";
|
|
|
+ }
|
|
|
+ } else if (isIndexAction(action)) {
|
|
|
+ final Collection<String> privileges = findIndexPrivilegesThatGrant(action);
|
|
|
+ if (privileges != null && privileges.size() > 0) {
|
|
|
+ message = message
|
|
|
+ + ", this action is granted by the index privileges ["
|
|
|
+ + collectionToCommaDelimitedString(privileges)
|
|
|
+ + "]";
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
+ return message;
|
|
|
}
|
|
|
|
|
|
- return message;
|
|
|
- }
|
|
|
+ @Override
|
|
|
+ public String remoteActionDenied(
|
|
|
+ Authentication authentication,
|
|
|
+ @Nullable AuthorizationEngine.AuthorizationInfo authorizationInfo,
|
|
|
+ String action,
|
|
|
+ String clusterAlias
|
|
|
+ ) {
|
|
|
+ assert isIndexAction(action);
|
|
|
+ String userText = successfulAuthenticationDescription(authentication, authorizationInfo);
|
|
|
+ String remoteClusterText = remoteClusterText(clusterAlias);
|
|
|
+ return actionIsUnauthorizedMessage(action, remoteClusterText, userText)
|
|
|
+ + " because no remote indices privileges apply for the target cluster";
|
|
|
+ }
|
|
|
|
|
|
- static String remoteActionDenied(
|
|
|
- Authentication authentication,
|
|
|
- @Nullable AuthorizationInfo authorizationInfo,
|
|
|
- String action,
|
|
|
- String clusterAlias
|
|
|
- ) {
|
|
|
- assert isIndexAction(action);
|
|
|
- String userText = successfulAuthenticationDescription(authentication, authorizationInfo);
|
|
|
- String remoteClusterText = remoteClusterText(clusterAlias);
|
|
|
- return actionIsUnauthorizedMessage(action, remoteClusterText, userText)
|
|
|
- + " because no remote indices privileges apply for the target cluster";
|
|
|
- }
|
|
|
+ protected Collection<String> findClusterPrivilegesThatGrant(
|
|
|
+ Authentication authentication,
|
|
|
+ String action,
|
|
|
+ TransportRequest request
|
|
|
+ ) {
|
|
|
+ return ClusterPrivilegeResolver.findPrivilegesThatGrant(action, request, authentication);
|
|
|
+ }
|
|
|
|
|
|
- private static String remoteClusterText(@Nullable String clusterAlias) {
|
|
|
- return Strings.format("towards remote cluster%s ", clusterAlias == null ? "" : " [" + clusterAlias + "]");
|
|
|
- }
|
|
|
+ protected Collection<String> findIndexPrivilegesThatGrant(String action) {
|
|
|
+ return IndexPrivilege.findPrivilegesThatGrant(action);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String remoteClusterText(@Nullable String clusterAlias) {
|
|
|
+ return Strings.format("towards remote cluster%s ", clusterAlias == null ? "" : " [" + clusterAlias + "]");
|
|
|
+ }
|
|
|
|
|
|
- private static String authenticatedUserDescription(Authentication authentication) {
|
|
|
- String userText = (authentication.isServiceAccount() ? "service account" : "user")
|
|
|
- + " ["
|
|
|
- + authentication.getAuthenticatingSubject().getUser().principal()
|
|
|
- + "]";
|
|
|
- if (authentication.isAuthenticatedAsApiKey() || authentication.isCrossClusterAccess()) {
|
|
|
- final String apiKeyId = (String) authentication.getAuthenticatingSubject()
|
|
|
- .getMetadata()
|
|
|
- .get(AuthenticationField.API_KEY_ID_KEY);
|
|
|
- assert apiKeyId != null : "api key id must be present in the metadata";
|
|
|
- userText = "API key id [" + apiKeyId + "] of " + userText;
|
|
|
- if (authentication.isCrossClusterAccess()) {
|
|
|
- final Authentication crossClusterAccessAuthentication = (Authentication) authentication.getAuthenticatingSubject()
|
|
|
+ private String authenticatedUserDescription(Authentication authentication) {
|
|
|
+ String userText = (authentication.isServiceAccount() ? "service account" : "user")
|
|
|
+ + " ["
|
|
|
+ + authentication.getAuthenticatingSubject().getUser().principal()
|
|
|
+ + "]";
|
|
|
+ if (authentication.isAuthenticatedAsApiKey() || authentication.isCrossClusterAccess()) {
|
|
|
+ final String apiKeyId = (String) authentication.getAuthenticatingSubject()
|
|
|
.getMetadata()
|
|
|
- .get(AuthenticationField.CROSS_CLUSTER_ACCESS_AUTHENTICATION_KEY);
|
|
|
- assert crossClusterAccessAuthentication != null : "cross cluster access authentication must be present in the metadata";
|
|
|
- userText = successfulAuthenticationDescription(crossClusterAccessAuthentication, null) + " authenticated by " + userText;
|
|
|
+ .get(AuthenticationField.API_KEY_ID_KEY);
|
|
|
+ assert apiKeyId != null : "api key id must be present in the metadata";
|
|
|
+ userText = "API key id [" + apiKeyId + "] of " + userText;
|
|
|
+ if (authentication.isCrossClusterAccess()) {
|
|
|
+ final Authentication crossClusterAccessAuthentication = (Authentication) authentication.getAuthenticatingSubject()
|
|
|
+ .getMetadata()
|
|
|
+ .get(AuthenticationField.CROSS_CLUSTER_ACCESS_AUTHENTICATION_KEY);
|
|
|
+ assert crossClusterAccessAuthentication != null : "cross cluster access authentication must be present in the metadata";
|
|
|
+ userText = successfulAuthenticationDescription(crossClusterAccessAuthentication, null)
|
|
|
+ + " authenticated by "
|
|
|
+ + userText;
|
|
|
+ }
|
|
|
}
|
|
|
+ return userText;
|
|
|
}
|
|
|
- return userText;
|
|
|
- }
|
|
|
|
|
|
- static String rolesDescription(Subject subject, @Nullable AuthorizationInfo authorizationInfo) {
|
|
|
- // We cannot print the roles if it's an API key or a service account (both do not have roles, but privileges)
|
|
|
- if (subject.getType() != Subject.Type.USER) {
|
|
|
- return "";
|
|
|
- }
|
|
|
+ // package-private for tests
|
|
|
+ String rolesDescription(Subject subject, @Nullable AuthorizationEngine.AuthorizationInfo authorizationInfo) {
|
|
|
+ // We cannot print the roles if it's an API key or a service account (both do not have roles, but privileges)
|
|
|
+ if (subject.getType() != Subject.Type.USER) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
|
|
|
- final StringBuilder sb = new StringBuilder();
|
|
|
- final List<String> effectiveRoleNames = extractEffectiveRoleNames(authorizationInfo);
|
|
|
- if (effectiveRoleNames == null) {
|
|
|
- sb.append(" with assigned roles [").append(Strings.arrayToCommaDelimitedString(subject.getUser().roles())).append("]");
|
|
|
- } else {
|
|
|
- sb.append(" with effective roles [").append(Strings.collectionToCommaDelimitedString(effectiveRoleNames)).append("]");
|
|
|
-
|
|
|
- final Set<String> assignedRoleNames = Set.of(subject.getUser().roles());
|
|
|
- final SortedSet<String> unfoundedRoleNames = Sets.sortedDifference(assignedRoleNames, Set.copyOf(effectiveRoleNames));
|
|
|
- if (false == unfoundedRoleNames.isEmpty()) {
|
|
|
- sb.append(" (assigned roles [")
|
|
|
- .append(Strings.collectionToCommaDelimitedString(unfoundedRoleNames))
|
|
|
- .append("] were not found)");
|
|
|
+ final StringBuilder sb = new StringBuilder();
|
|
|
+ final List<String> effectiveRoleNames = extractEffectiveRoleNames(authorizationInfo);
|
|
|
+ if (effectiveRoleNames == null) {
|
|
|
+ sb.append(" with assigned roles [").append(Strings.arrayToCommaDelimitedString(subject.getUser().roles())).append("]");
|
|
|
+ } else {
|
|
|
+ sb.append(" with effective roles [").append(Strings.collectionToCommaDelimitedString(effectiveRoleNames)).append("]");
|
|
|
+
|
|
|
+ final Set<String> assignedRoleNames = Set.of(subject.getUser().roles());
|
|
|
+ final SortedSet<String> unfoundedRoleNames = Sets.sortedDifference(assignedRoleNames, Set.copyOf(effectiveRoleNames));
|
|
|
+ if (false == unfoundedRoleNames.isEmpty()) {
|
|
|
+ sb.append(" (assigned roles [")
|
|
|
+ .append(Strings.collectionToCommaDelimitedString(unfoundedRoleNames))
|
|
|
+ .append("] were not found)");
|
|
|
+ }
|
|
|
}
|
|
|
+ return sb.toString();
|
|
|
}
|
|
|
- return sb.toString();
|
|
|
- }
|
|
|
|
|
|
- static String successfulAuthenticationDescription(Authentication authentication, @Nullable AuthorizationInfo authorizationInfo) {
|
|
|
- String userText = authenticatedUserDescription(authentication);
|
|
|
+ // package-private for tests
|
|
|
+ String successfulAuthenticationDescription(
|
|
|
+ Authentication authentication,
|
|
|
+ @Nullable AuthorizationEngine.AuthorizationInfo authorizationInfo
|
|
|
+ ) {
|
|
|
+ String userText = authenticatedUserDescription(authentication);
|
|
|
|
|
|
- if (authentication.isRunAs()) {
|
|
|
- userText = userText + " run as [" + authentication.getEffectiveSubject().getUser().principal() + "]";
|
|
|
+ if (authentication.isRunAs()) {
|
|
|
+ userText = userText + " run as [" + authentication.getEffectiveSubject().getUser().principal() + "]";
|
|
|
+ }
|
|
|
+
|
|
|
+ userText += rolesDescription(authentication.getEffectiveSubject(), authorizationInfo);
|
|
|
+ return userText;
|
|
|
}
|
|
|
|
|
|
- userText += rolesDescription(authentication.getEffectiveSubject(), authorizationInfo);
|
|
|
- return userText;
|
|
|
- }
|
|
|
+ private List<String> extractEffectiveRoleNames(@Nullable AuthorizationEngine.AuthorizationInfo authorizationInfo) {
|
|
|
+ if (authorizationInfo == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
|
|
|
- private static List<String> extractEffectiveRoleNames(@Nullable AuthorizationInfo authorizationInfo) {
|
|
|
- if (authorizationInfo == null) {
|
|
|
- return null;
|
|
|
+ final Map<String, Object> info = authorizationInfo.asMap();
|
|
|
+ final Object roleNames = info.get(PRINCIPAL_ROLES_FIELD_NAME);
|
|
|
+ // AuthorizationInfo from custom authorization engine may not have this field or have it as a different data type
|
|
|
+ if (false == roleNames instanceof String[]) {
|
|
|
+ assert false == authorizationInfo instanceof RBACEngine.RBACAuthorizationInfo
|
|
|
+ : "unexpected user.roles field [" + roleNames + "] for RBACAuthorizationInfo";
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return Arrays.stream((String[]) roleNames).sorted().toList();
|
|
|
}
|
|
|
|
|
|
- final Map<String, Object> info = authorizationInfo.asMap();
|
|
|
- final Object roleNames = info.get(PRINCIPAL_ROLES_FIELD_NAME);
|
|
|
- // AuthorizationInfo from custom authorization engine may not have this field or have it as a different data type
|
|
|
- if (false == roleNames instanceof String[]) {
|
|
|
- assert false == authorizationInfo instanceof RBACEngine.RBACAuthorizationInfo
|
|
|
- : "unexpected user.roles field [" + roleNames + "] for RBACAuthorizationInfo";
|
|
|
- return null;
|
|
|
+ private String actionIsUnauthorizedMessage(String action, String userText) {
|
|
|
+ return actionIsUnauthorizedMessage(action, "", userText);
|
|
|
}
|
|
|
- return Arrays.stream((String[]) roleNames).sorted().toList();
|
|
|
- }
|
|
|
-
|
|
|
- private static String actionIsUnauthorizedMessage(String action, String userText) {
|
|
|
- return actionIsUnauthorizedMessage(action, "", userText);
|
|
|
- }
|
|
|
|
|
|
- private static String actionIsUnauthorizedMessage(String action, String remoteClusterText, String userText) {
|
|
|
- return "action [" + action + "] " + remoteClusterText + "is unauthorized for " + userText;
|
|
|
+ private String actionIsUnauthorizedMessage(String action, String remoteClusterText, String userText) {
|
|
|
+ return "action [" + action + "] " + remoteClusterText + "is unauthorized for " + userText;
|
|
|
+ }
|
|
|
}
|
|
|
}
|