InfraRuleBasedAuthorizationPlugin.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. package org.apache.ambari.infra.security;
  20. import com.google.common.collect.ImmutableSet;
  21. import java.io.IOException;
  22. import java.lang.invoke.MethodHandles;
  23. import java.security.Principal;
  24. import java.util.ArrayList;
  25. import java.util.Collection;
  26. import java.util.Collections;
  27. import java.util.HashMap;
  28. import java.util.HashSet;
  29. import java.util.LinkedHashMap;
  30. import java.util.List;
  31. import java.util.Map;
  32. import java.util.Objects;
  33. import java.util.Set;
  34. import org.apache.solr.common.SolrException;
  35. import org.apache.solr.common.params.CollectionParams;
  36. import org.apache.solr.common.util.Utils;
  37. import org.apache.solr.security.AuthorizationContext;
  38. import org.apache.solr.security.AuthorizationPlugin;
  39. import org.apache.solr.security.AuthorizationResponse;
  40. import org.apache.solr.security.ConfigEditablePlugin;
  41. import org.apache.solr.util.CommandOperation;
  42. import org.slf4j.Logger;
  43. import org.slf4j.LoggerFactory;
  44. import static java.util.Collections.singleton;
  45. import static org.apache.solr.common.params.CommonParams.NAME;
  46. import static org.apache.solr.common.util.Utils.getDeepCopy;
  47. import static org.apache.solr.handler.admin.SecurityConfHandler.getListValue;
  48. import static org.apache.solr.handler.admin.SecurityConfHandler.getMapValue;
  49. /**
  50. * Modified copy of solr.RuleBasedAuthorizationPlugin to handle role - permission mappings with KereberosPlugin
  51. * Added 2 new JSON map: (precedence: user-host-regex > user-host)
  52. * 1. "user-host": user host mappings (array) for hostname validation
  53. * 2. "user-host-regex": user host regex mapping (string) for hostname validation
  54. */
  55. public class InfraRuleBasedAuthorizationPlugin implements AuthorizationPlugin, ConfigEditablePlugin {
  56. private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
  57. private final Map<String, Set<String>> usersVsRoles = new HashMap<>();
  58. private final Map<String, WildCardSupportMap> mapping = new HashMap<>();
  59. private final List<Permission> permissions = new ArrayList<>();
  60. private final Map<String, Set<String>> userVsHosts = new HashMap<>();
  61. private final Map<String, String> userVsHostRegex = new HashMap<>();
  62. private final InfraUserRolesLookupStrategy infraUserRolesLookupStrategy = new InfraUserRolesLookupStrategy();
  63. private final InfraKerberosHostValidator infraKerberosDomainValidator = new InfraKerberosHostValidator();
  64. private static class WildCardSupportMap extends HashMap<String, List<Permission>> {
  65. final Set<String> wildcardPrefixes = new HashSet<>();
  66. @Override
  67. public List<Permission> put(String key, List<Permission> value) {
  68. if (key != null && key.endsWith("/*")) {
  69. key = key.substring(0, key.length() - 2);
  70. wildcardPrefixes.add(key);
  71. }
  72. return super.put(key, value);
  73. }
  74. @Override
  75. public List<Permission> get(Object key) {
  76. List<Permission> result = super.get(key);
  77. if (key == null || result != null) return result;
  78. if (!wildcardPrefixes.isEmpty()) {
  79. for (String s : wildcardPrefixes) {
  80. if (key.toString().startsWith(s)) {
  81. List<Permission> l = super.get(s);
  82. if (l != null) {
  83. result = result == null ? new ArrayList<Permission>() : new ArrayList<Permission>(result);
  84. result.addAll(l);
  85. }
  86. }
  87. }
  88. }
  89. return result;
  90. }
  91. }
  92. @Override
  93. public AuthorizationResponse authorize(AuthorizationContext context) {
  94. List<AuthorizationContext.CollectionRequest> collectionRequests = context.getCollectionRequests();
  95. if (context.getRequestType() == AuthorizationContext.RequestType.ADMIN) {
  96. MatchStatus flag = checkCollPerm(mapping.get(null), context);
  97. return flag.rsp;
  98. }
  99. for (AuthorizationContext.CollectionRequest collreq : collectionRequests) {
  100. //check permissions for each collection
  101. MatchStatus flag = checkCollPerm(mapping.get(collreq.collectionName), context);
  102. if (flag != MatchStatus.NO_PERMISSIONS_FOUND) return flag.rsp;
  103. }
  104. //check wildcard (all=*) permissions.
  105. MatchStatus flag = checkCollPerm(mapping.get("*"), context);
  106. return flag.rsp;
  107. }
  108. private MatchStatus checkCollPerm(Map<String, List<Permission>> pathVsPerms,
  109. AuthorizationContext context) {
  110. if (pathVsPerms == null) return MatchStatus.NO_PERMISSIONS_FOUND;
  111. String path = context.getResource();
  112. MatchStatus flag = checkPathPerm(pathVsPerms.get(path), context);
  113. if (flag != MatchStatus.NO_PERMISSIONS_FOUND) return flag;
  114. return checkPathPerm(pathVsPerms.get(null), context);
  115. }
  116. private MatchStatus checkPathPerm(List<Permission> permissions, AuthorizationContext context) {
  117. if (permissions == null || permissions.isEmpty()) return MatchStatus.NO_PERMISSIONS_FOUND;
  118. Principal principal = context.getUserPrincipal();
  119. loopPermissions:
  120. for (int i = 0; i < permissions.size(); i++) {
  121. Permission permission = permissions.get(i);
  122. if (permission.method != null && !permission.method.contains(context.getHttpMethod())) {
  123. //this permissions HTTP method does not match this rule. try other rules
  124. continue;
  125. }
  126. if(permission.predicate != null){
  127. if(!permission.predicate.test(context)) continue ;
  128. }
  129. if (permission.params != null) {
  130. for (Map.Entry<String, Object> e : permission.params.entrySet()) {
  131. String paramVal = context.getParams().get(e.getKey());
  132. Object val = e.getValue();
  133. if (val instanceof List) {
  134. if (!((List) val).contains(paramVal)) continue loopPermissions;
  135. } else if (!Objects.equals(val, paramVal)) continue loopPermissions;
  136. }
  137. }
  138. if (permission.role == null) {
  139. //no role is assigned permission.That means everybody is allowed to access
  140. return MatchStatus.PERMITTED;
  141. }
  142. if (principal == null) {
  143. log.info("request has come without principal. failed permission {} ",permission);
  144. //this resource needs a principal but the request has come without
  145. //any credential.
  146. return MatchStatus.USER_REQUIRED;
  147. } else if (permission.role.contains("*")) {
  148. return MatchStatus.PERMITTED;
  149. }
  150. for (String role : permission.role) {
  151. Set<String> userRoles = infraUserRolesLookupStrategy.getUserRolesFromPrincipal(usersVsRoles, principal);
  152. boolean validHostname = infraKerberosDomainValidator.validate(principal, userVsHosts, userVsHostRegex);
  153. if (!validHostname) {
  154. log.warn("Hostname is not valid for principal {}", principal);
  155. return MatchStatus.FORBIDDEN;
  156. }
  157. if (userRoles != null && userRoles.contains(role)) return MatchStatus.PERMITTED;
  158. }
  159. log.info("This resource is configured to have a permission {}, The principal {} does not have the right role ", permission, principal);
  160. return MatchStatus.FORBIDDEN;
  161. }
  162. log.debug("No permissions configured for the resource {} . So allowed to access", context.getResource());
  163. return MatchStatus.NO_PERMISSIONS_FOUND;
  164. }
  165. @Override
  166. public void init(Map<String, Object> initInfo) {
  167. mapping.put(null, new WildCardSupportMap());
  168. Map<String, Object> map = getMapValue(initInfo, "user-role");
  169. for (Object o : map.entrySet()) {
  170. Map.Entry e = (Map.Entry) o;
  171. String roleName = (String) e.getKey();
  172. usersVsRoles.put(roleName, readValueAsSet(map, roleName));
  173. }
  174. List<Map> perms = getListValue(initInfo, "permissions");
  175. for (Map o : perms) {
  176. Permission p;
  177. try {
  178. p = Permission.load(o);
  179. } catch (Exception exp) {
  180. log.error("Invalid permission ", exp);
  181. continue;
  182. }
  183. permissions.add(p);
  184. add2Mapping(p);
  185. }
  186. // adding user-host
  187. Map<String, Object> userHostsMap = getMapValue(initInfo, "user-host");
  188. for (Object userHost : userHostsMap.entrySet()) {
  189. Map.Entry e = (Map.Entry) userHost;
  190. String roleName = (String) e.getKey();
  191. userVsHosts.put(roleName, readValueAsSet(userHostsMap, roleName));
  192. }
  193. // adding user-host-regex
  194. Map<String, Object> userHostRegexMap = getMapValue(initInfo, "user-host-regex");
  195. for (Map.Entry<String, Object> entry : userHostRegexMap.entrySet()) {
  196. userVsHostRegex.put(entry.getKey(), entry.getValue().toString());
  197. }
  198. }
  199. //this is to do optimized lookup of permissions for a given collection/path
  200. private void add2Mapping(Permission permission) {
  201. for (String c : permission.collections) {
  202. WildCardSupportMap m = mapping.get(c);
  203. if (m == null) mapping.put(c, m = new WildCardSupportMap());
  204. for (String path : permission.path) {
  205. List<Permission> perms = m.get(path);
  206. if (perms == null) m.put(path, perms = new ArrayList<>());
  207. perms.add(permission);
  208. }
  209. }
  210. }
  211. /**
  212. * read a key value as a set. if the value is a single string ,
  213. * return a singleton set
  214. *
  215. * @param m the map from which to lookup
  216. * @param key the key with which to do lookup
  217. */
  218. static Set<String> readValueAsSet(Map m, String key) {
  219. Set<String> result = new HashSet<>();
  220. Object val = m.get(key);
  221. if (val == null) {
  222. if("collection".equals(key)){
  223. //for collection collection: null means a core admin/ collection admin request
  224. // otherwise it means a request where collection name is ignored
  225. return m.containsKey(key) ? singleton((String) null) : singleton("*");
  226. }
  227. return null;
  228. }
  229. if (val instanceof Collection) {
  230. Collection list = (Collection) val;
  231. for (Object o : list) result.add(String.valueOf(o));
  232. } else if (val instanceof String) {
  233. result.add((String) val);
  234. } else {
  235. throw new RuntimeException("Bad value for : " + key);
  236. }
  237. return result.isEmpty() ? null : Collections.unmodifiableSet(result);
  238. }
  239. @Override
  240. public void close() throws IOException { }
  241. static class Permission {
  242. String name;
  243. Set<String> path, role, collections, method;
  244. Map<String, Object> params;
  245. Predicate<AuthorizationContext> predicate;
  246. Map originalConfig;
  247. private Permission() {
  248. }
  249. static Permission load(Map m) {
  250. Permission p = new Permission();
  251. p.originalConfig = new LinkedHashMap<>(m);
  252. String name = (String) m.get(NAME);
  253. if (!m.containsKey("role")) throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "role not specified");
  254. p.role = readValueAsSet(m, "role");
  255. if (well_known_permissions.containsKey(name)) {
  256. HashSet<String> disAllowed = new HashSet<>(knownKeys);
  257. disAllowed.remove("role");//these are the only
  258. disAllowed.remove(NAME);//allowed keys for well-known permissions
  259. for (String s : disAllowed) {
  260. if (m.containsKey(s))
  261. throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, s + " is not a valid key for the permission : " + name);
  262. }
  263. p.predicate = (Predicate<AuthorizationContext>) ((Map) well_known_permissions.get(name)).get(Predicate.class.getName());
  264. m = well_known_permissions.get(name);
  265. }
  266. p.name = name;
  267. p.path = readSetSmart(name, m, "path");
  268. p.collections = readSetSmart(name, m, "collection");
  269. p.method = readSetSmart(name, m, "method");
  270. p.params = (Map<String, Object>) m.get("params");
  271. return p;
  272. }
  273. @Override
  274. public String toString() {
  275. return Utils.toJSONString(originalConfig);
  276. }
  277. static final Set<String> knownKeys = ImmutableSet.of("collection", "role", "params", "path", "method", NAME);
  278. }
  279. enum MatchStatus {
  280. USER_REQUIRED(AuthorizationResponse.PROMPT),
  281. NO_PERMISSIONS_FOUND(AuthorizationResponse.OK),
  282. PERMITTED(AuthorizationResponse.OK),
  283. FORBIDDEN(AuthorizationResponse.FORBIDDEN);
  284. final AuthorizationResponse rsp;
  285. MatchStatus(AuthorizationResponse rsp) {
  286. this.rsp = rsp;
  287. }
  288. }
  289. /**
  290. * This checks for the defaults available other rules for the keys
  291. */
  292. private static Set<String> readSetSmart(String permissionName, Map m, String key) {
  293. Set<String> set = readValueAsSet(m, key);
  294. if (set == null && well_known_permissions.containsKey(permissionName)) {
  295. set = readValueAsSet((Map) well_known_permissions.get(permissionName), key);
  296. }
  297. if ("method".equals(key)) {
  298. if (set != null) {
  299. for (String s : set) if (!HTTP_METHODS.contains(s)) return null;
  300. }
  301. return set;
  302. }
  303. return set == null ? singleton((String)null) : set;
  304. }
  305. @Override
  306. public Map<String, Object> edit(Map<String, Object> latestConf, List<CommandOperation> commands) {
  307. for (CommandOperation op : commands) {
  308. OPERATION operation = null;
  309. for (OPERATION o : OPERATION.values()) {
  310. if (o.name.equals(op.name)) {
  311. operation = o;
  312. break;
  313. }
  314. }
  315. if (operation == null) {
  316. op.unknownOperation();
  317. return null;
  318. }
  319. latestConf = operation.edit(latestConf, op);
  320. if (latestConf == null) return null;
  321. }
  322. return latestConf;
  323. }
  324. enum OPERATION {
  325. SET_USER_ROLE("set-user-role") {
  326. @Override
  327. public Map<String, Object> edit(Map<String, Object> latestConf, CommandOperation op) {
  328. Map<String, Object> roleMap = getMapValue(latestConf, "user-role");
  329. Map<String, Object> map = op.getDataMap();
  330. if (op.hasError()) return null;
  331. for (Map.Entry<String, Object> e : map.entrySet()) {
  332. if (e.getValue() == null) {
  333. roleMap.remove(e.getKey());
  334. continue;
  335. }
  336. if (e.getValue() instanceof String || e.getValue() instanceof List) {
  337. roleMap.put(e.getKey(), e.getValue());
  338. } else {
  339. op.addError("Unexpected value ");
  340. return null;
  341. }
  342. }
  343. return latestConf;
  344. }
  345. },
  346. SET_PERMISSION("set-permission") {
  347. @Override
  348. public Map<String, Object> edit(Map<String, Object> latestConf, CommandOperation op) {
  349. String name = op.getStr(NAME);
  350. Map<String, Object> dataMap = op.getDataMap();
  351. if (op.hasError()) return null;
  352. dataMap = getDeepCopy(dataMap, 3);
  353. String before = (String) dataMap.remove("before");
  354. for (String key : dataMap.keySet()) {
  355. if (!Permission.knownKeys.contains(key)) op.addError("Unknown key, " + key);
  356. }
  357. try {
  358. Permission.load(dataMap);
  359. } catch (Exception e) {
  360. op.addError(e.getMessage());
  361. return null;
  362. }
  363. List<Map> permissions = getListValue(latestConf, "permissions");
  364. List<Map> permissionsCopy = new ArrayList<>();
  365. boolean added = false;
  366. for (Map e : permissions) {
  367. Object n = e.get(NAME);
  368. if (n.equals(before) || n.equals(name)) {
  369. added = true;
  370. permissionsCopy.add(dataMap);
  371. }
  372. if (!n.equals(name)) permissionsCopy.add(e);
  373. }
  374. if (!added && before != null) {
  375. op.addError("Invalid 'before' :" + before);
  376. return null;
  377. }
  378. if (!added) permissionsCopy.add(dataMap);
  379. latestConf.put("permissions", permissionsCopy);
  380. return latestConf;
  381. }
  382. },
  383. UPDATE_PERMISSION("update-permission") {
  384. @Override
  385. public Map<String, Object> edit(Map<String, Object> latestConf, CommandOperation op) {
  386. String name = op.getStr(NAME);
  387. if (op.hasError()) return null;
  388. for (Map permission : (List<Map>) getListValue(latestConf, "permissions")) {
  389. if (name.equals(permission.get(NAME))) {
  390. LinkedHashMap copy = new LinkedHashMap<>(permission);
  391. copy.putAll(op.getDataMap());
  392. op.setCommandData(copy);
  393. return SET_PERMISSION.edit(latestConf, op);
  394. }
  395. }
  396. op.addError("No such permission " + name);
  397. return null;
  398. }
  399. },
  400. DELETE_PERMISSION("delete-permission") {
  401. @Override
  402. public Map<String, Object> edit(Map<String, Object> latestConf, CommandOperation op) {
  403. List<String> names = op.getStrs("");
  404. if (names == null || names.isEmpty()) {
  405. op.addError("Invalid command");
  406. return null;
  407. }
  408. names = new ArrayList<>(names);
  409. List<Map> copy = new ArrayList<>();
  410. List<Map> p = getListValue(latestConf, "permissions");
  411. for (Map map : p) {
  412. Object n = map.get(NAME);
  413. if (names.contains(n)) {
  414. names.remove(n);
  415. continue;
  416. } else {
  417. copy.add(map);
  418. }
  419. }
  420. if (!names.isEmpty()) {
  421. op.addError("Unknown permission name(s) " + names);
  422. return null;
  423. }
  424. latestConf.put("permissions", copy);
  425. return latestConf;
  426. }
  427. };
  428. public abstract Map<String, Object> edit(Map<String, Object> latestConf, CommandOperation op);
  429. public final String name;
  430. OPERATION(String s) {
  431. this.name = s;
  432. }
  433. public static OPERATION get(String name) {
  434. for (OPERATION o : values()) if (o.name.equals(name)) return o;
  435. return null;
  436. }
  437. }
  438. public static final Set<String> HTTP_METHODS = ImmutableSet.of("GET", "POST", "DELETE", "PUT", "HEAD");
  439. private static final Map<String, Map<String,Object>> well_known_permissions = (Map) Utils.fromJSONString(
  440. " { " +
  441. " security-edit :{" +
  442. " path:['/admin/authentication','/admin/authorization']," +
  443. " collection:null," +
  444. " method:POST }," +
  445. " security-read :{" +
  446. " path:['/admin/authentication','/admin/authorization']," +
  447. " collection:null," +
  448. " method:GET}," +
  449. " schema-edit :{" +
  450. " method:POST," +
  451. " path:'/schema/*'}," +
  452. " collection-admin-edit :{" +
  453. " collection:null," +
  454. " path:'/admin/collections'}," +
  455. " collection-admin-read :{" +
  456. " collection:null," +
  457. " path:'/admin/collections'}," +
  458. " schema-read :{" +
  459. " method:GET," +
  460. " path:'/schema/*'}," +
  461. " config-read :{" +
  462. " method:GET," +
  463. " path:'/config/*'}," +
  464. " update :{" +
  465. " path:'/update/*'}," +
  466. " read :{" +
  467. " path:['/select', '/get','/browse','/tvrh','/terms','/clustering','/elevate', '/export','/spell','/clustering']}," +
  468. " config-edit:{" +
  469. " method:POST," +
  470. " path:'/config/*'}," +
  471. " all:{collection:['*', null]}" +
  472. "}");
  473. static {
  474. ((Map) well_known_permissions.get("collection-admin-edit")).put(Predicate.class.getName(), getCollectionActionPredicate(true));
  475. ((Map) well_known_permissions.get("collection-admin-read")).put(Predicate.class.getName(), getCollectionActionPredicate(false));
  476. }
  477. private static Predicate<AuthorizationContext> getCollectionActionPredicate(final boolean isEdit) {
  478. return new Predicate<AuthorizationContext>() {
  479. @Override
  480. public boolean test(AuthorizationContext context) {
  481. String action = context.getParams().get("action");
  482. if (action == null) return false;
  483. CollectionParams.CollectionAction collectionAction = CollectionParams.CollectionAction.get(action);
  484. if (collectionAction == null) return false;
  485. return isEdit ? collectionAction.isWrite : !collectionAction.isWrite;
  486. }
  487. };
  488. }
  489. public static void main(String[] args) {
  490. System.out.println(Utils.toJSONString(well_known_permissions));
  491. }
  492. public interface Predicate<T> {
  493. boolean test(T t);
  494. }
  495. }