|
| 1 | +/* |
| 2 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 3 | + * you may not use this file except in compliance with the License. |
| 4 | + * You may obtain a copy of the License at |
| 5 | + * |
| 6 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 7 | + * |
| 8 | + * Unless required by applicable law or agreed to in writing, software |
| 9 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 10 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 11 | + * See the License for the specific language governing permissions and |
| 12 | + * limitations under the License. |
| 13 | + */ |
| 14 | +package io.trino.plugin.base.security; |
| 15 | + |
| 16 | +import io.trino.spi.TrinoException; |
| 17 | + |
| 18 | +import java.util.Optional; |
| 19 | +import java.util.Set; |
| 20 | +import java.util.regex.Matcher; |
| 21 | +import java.util.regex.Pattern; |
| 22 | + |
| 23 | +import static io.trino.spi.StandardErrorCode.CONFIGURATION_INVALID; |
| 24 | + |
| 25 | +public class ReplacePatternMatcher |
| 26 | +{ |
| 27 | + private final Optional<Pattern> userRegex; |
| 28 | + private final Optional<Pattern> roleRegex; |
| 29 | + private final Optional<Pattern> groupRegex; |
| 30 | + |
| 31 | + private final String user; |
| 32 | + private final Set<String> roles; |
| 33 | + private final Set<String> groups; |
| 34 | + |
| 35 | + private final Optional<Matcher> userMatcher; |
| 36 | + private final Optional<Matcher> roleMatcher; |
| 37 | + private final Optional<Matcher> groupMatcher; |
| 38 | + |
| 39 | + public ReplacePatternMatcher(Optional<Pattern> userRegex, Optional<Pattern> roleRegex, Optional<Pattern> groupRegex, String user, Set<String> roles, Set<String> groups) |
| 40 | + { |
| 41 | + this.userRegex = userRegex; |
| 42 | + this.roleRegex = roleRegex; |
| 43 | + this.groupRegex = groupRegex; |
| 44 | + |
| 45 | + this.user = user; |
| 46 | + this.roles = roles; |
| 47 | + this.groups = groups; |
| 48 | + |
| 49 | + this.userMatcher = userRegex.map(regex -> regex.matcher(user)); |
| 50 | + this.roleMatcher = roleRegex.flatMap(regex -> roles.stream().map(regex::matcher).filter(Matcher::matches).findAny()); |
| 51 | + this.groupMatcher = groupRegex.flatMap(regex -> groups.stream().map(regex::matcher).filter(Matcher::matches).findAny()); |
| 52 | + } |
| 53 | + |
| 54 | + private boolean match() |
| 55 | + { |
| 56 | + return userRegex.map(regex -> regex.matcher(user).matches()).orElse(true) && |
| 57 | + roleRegex.map(regex -> roles.stream().anyMatch(role -> regex.matcher(role).matches())).orElse(true) && |
| 58 | + groupRegex.map(regex -> groups.stream().anyMatch(group -> regex.matcher(group).matches())).orElse(true); |
| 59 | + } |
| 60 | + |
| 61 | + private boolean matchCatalogInternal(final Optional<Pattern> catalogRegex, String catalog) |
| 62 | + { |
| 63 | + validateMatchers(userMatcher, roleMatcher, groupMatcher); |
| 64 | + |
| 65 | + Optional<Pattern> userReplacedCatalogPattern = replaceRegex(userMatcher, catalogRegex, "catalog", "user"); |
| 66 | + Optional<Pattern> roleReplacedCatalogPattern = replaceRegex(roleMatcher, catalogRegex, "catalog", "role"); |
| 67 | + Optional<Pattern> groupReplacedCatalogPattern = replaceRegex(groupMatcher, catalogRegex, "catalog", "group"); |
| 68 | + |
| 69 | + return matchPattern(userReplacedCatalogPattern, catalog) || matchPattern(roleReplacedCatalogPattern, catalog) |
| 70 | + || matchPattern(groupReplacedCatalogPattern, catalog); |
| 71 | + } |
| 72 | + |
| 73 | + public boolean matchCatalog(final Optional<Pattern> catalogRegex, String catalog) |
| 74 | + { |
| 75 | + return match() && matchCatalogInternal(catalogRegex, catalog); |
| 76 | + } |
| 77 | + |
| 78 | + private boolean matchSchemaInternal(final Optional<Pattern> schemaRegex, String schema) |
| 79 | + { |
| 80 | + validateMatchers(userMatcher, roleMatcher, groupMatcher); |
| 81 | + |
| 82 | + Optional<Pattern> userReplacedSchemaPattern = replaceRegex(userMatcher, schemaRegex, "schema", "user"); |
| 83 | + Optional<Pattern> roleReplacedSchemaPattern = replaceRegex(roleMatcher, schemaRegex, "schema", "role"); |
| 84 | + Optional<Pattern> groupReplacedSchemaPattern = replaceRegex(groupMatcher, schemaRegex, "schema", "group"); |
| 85 | + |
| 86 | + return matchPattern(userReplacedSchemaPattern, schema) || matchPattern(roleReplacedSchemaPattern, schema) |
| 87 | + || matchPattern(groupReplacedSchemaPattern, schema); |
| 88 | + } |
| 89 | + |
| 90 | + public boolean matchSchema(final Optional<Pattern> schemaRegex, String schema) |
| 91 | + { |
| 92 | + return match() && matchSchemaInternal(schemaRegex, schema); |
| 93 | + } |
| 94 | + |
| 95 | + public boolean matchCatalogAndSchema(final Optional<Pattern> catalogRegex, String catalog, final Optional<Pattern> schemaRegex, String schema) |
| 96 | + { |
| 97 | + return match() && matchCatalogInternal(catalogRegex, catalog) && matchSchemaInternal(schemaRegex, schema); |
| 98 | + } |
| 99 | + |
| 100 | + private boolean matchTableInternal(final Optional<Pattern> tableRegex, String table) |
| 101 | + { |
| 102 | + validateMatchers(userMatcher, roleMatcher, groupMatcher); |
| 103 | + |
| 104 | + Optional<Pattern> userReplacedTablePattern = replaceRegex(userMatcher, tableRegex, "table", "user"); |
| 105 | + Optional<Pattern> roleReplacedTablePattern = replaceRegex(roleMatcher, tableRegex, "table", "role"); |
| 106 | + Optional<Pattern> groupReplacedTablePattern = replaceRegex(groupMatcher, tableRegex, "table", "group"); |
| 107 | + |
| 108 | + return matchPattern(userReplacedTablePattern, table) || matchPattern(roleReplacedTablePattern, table) |
| 109 | + || matchPattern(groupReplacedTablePattern, table); |
| 110 | + } |
| 111 | + |
| 112 | + public boolean matchTable(final Optional<Pattern> tableRegex, String table) |
| 113 | + { |
| 114 | + return match() && matchTableInternal(tableRegex, table); |
| 115 | + } |
| 116 | + |
| 117 | + public boolean matchSchemaAndTable(final Optional<Pattern> schemaRegex, String schema, final Optional<Pattern> tableRegex, String table) |
| 118 | + { |
| 119 | + return match() && matchSchemaInternal(schemaRegex, schema) && matchTableInternal(tableRegex, table); |
| 120 | + } |
| 121 | + |
| 122 | + public boolean matchQueryAccessRule(final Optional<Pattern> queryOwnerRegex, Optional<String> queryOwner) |
| 123 | + { |
| 124 | + if (queryOwner.isEmpty() && queryOwnerRegex.isEmpty()) { |
| 125 | + return true; |
| 126 | + } |
| 127 | + |
| 128 | + validateMatchers(userMatcher, roleMatcher, groupMatcher); |
| 129 | + |
| 130 | + Optional<Pattern> userReplacedQueryAccessRulePattern = replaceRegex(userMatcher, queryOwnerRegex, "query_owner", "user"); |
| 131 | + Optional<Pattern> roleReplacedQueryAccessRulePattern = replaceRegex(roleMatcher, queryOwnerRegex, "query_owner", "role"); |
| 132 | + Optional<Pattern> groupReplacedQueryAccessRulePattern = replaceRegex(groupMatcher, queryOwnerRegex, "query_owner", "group"); |
| 133 | + |
| 134 | + return match() && queryOwner.isPresent() && (matchPattern(userReplacedQueryAccessRulePattern, queryOwner.get()) |
| 135 | + || matchPattern(roleReplacedQueryAccessRulePattern, queryOwner.get()) || matchPattern(groupReplacedQueryAccessRulePattern, queryOwner.get())); |
| 136 | + } |
| 137 | + |
| 138 | + // check if more than one matcher is used to replace patterns |
| 139 | + private void validateMatchers(Optional<Matcher> userMatcher, Optional<Matcher> roleMatcher, Optional<Matcher> groupMatcher) |
| 140 | + { |
| 141 | + if ((userMatcher.isPresent() && userMatcher.get().matches() && userMatcher.get().groupCount() > 0 ? 1 : 0) + |
| 142 | + (roleMatcher.isPresent() && roleMatcher.get().matches() && roleMatcher.get().groupCount() > 0 ? 1 : 0) + |
| 143 | + (groupMatcher.isPresent() && groupMatcher.get().matches() && groupMatcher.get().groupCount() > 0 ? 1 : 0) > 1) { |
| 144 | + throw new TrinoException(CONFIGURATION_INVALID, "Multiple matchers that contain capturing groups are used" + |
| 145 | + " to replace patterns. This may lead to unexpected results."); |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + private Optional<Pattern> replaceRegex(Optional<Matcher> matcher, Optional<Pattern> patternToReplace, String toReplace, String capturingGroup) |
| 150 | + { |
| 151 | + if (matcher.isEmpty()) { |
| 152 | + return patternToReplace; |
| 153 | + } |
| 154 | + if (patternToReplace.isPresent() && patternToReplace.get().pattern().matches(".*\\$\\d+.*")) { |
| 155 | + if (matcher.get().matches() && matcher.get().groupCount() > 0) { |
| 156 | + StringBuilder stringBuilder = new StringBuilder(); |
| 157 | + try { |
| 158 | + matcher.get().appendReplacement(stringBuilder, patternToReplace.get().pattern()); |
| 159 | + } |
| 160 | + catch (IndexOutOfBoundsException e) { |
| 161 | + throw new TrinoException(CONFIGURATION_INVALID, |
| 162 | + toReplace + " in replace pattern refers to a capturing group that does not exist in " + capturingGroup, e); |
| 163 | + } |
| 164 | + return Optional.of(Pattern.compile(stringBuilder.toString())); |
| 165 | + } |
| 166 | + } |
| 167 | + return patternToReplace; |
| 168 | + } |
| 169 | + |
| 170 | + private boolean matchPattern(Optional<Pattern> pattern, String value) |
| 171 | + { |
| 172 | + return pattern.map(regex -> regex.matcher(value).matches()).orElse(true); |
| 173 | + } |
| 174 | +} |
0 commit comments