001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 019package org.apache.oozie.service; 020 021import java.io.BufferedReader; 022import java.io.File; 023import java.io.FileInputStream; 024import java.io.FileNotFoundException; 025import java.io.IOException; 026import java.io.InputStreamReader; 027import java.net.URI; 028import java.util.HashSet; 029import java.util.List; 030import java.util.Map; 031import java.util.ArrayList; 032import java.util.Set; 033 034import org.apache.commons.lang.StringUtils; 035import org.apache.hadoop.conf.Configuration; 036import org.apache.hadoop.fs.FileSystem; 037import org.apache.hadoop.fs.Path; 038import org.apache.hadoop.security.AccessControlException; 039import org.apache.oozie.BundleJobBean; 040import org.apache.oozie.CoordinatorJobBean; 041import org.apache.oozie.ErrorCode; 042import org.apache.oozie.WorkflowJobBean; 043import org.apache.oozie.client.XOozieClient; 044import org.apache.oozie.executor.jpa.BundleJobGetJPAExecutor; 045import org.apache.oozie.executor.jpa.CoordJobInfoGetJPAExecutor; 046import org.apache.oozie.executor.jpa.BundleJobInfoGetJPAExecutor; 047import org.apache.oozie.executor.jpa.WorkflowsJobGetJPAExecutor; 048import org.apache.oozie.executor.jpa.CoordJobGetJPAExecutor; 049import org.apache.oozie.executor.jpa.JPAExecutorException; 050import org.apache.oozie.executor.jpa.WorkflowJobQueryExecutor; 051import org.apache.oozie.executor.jpa.WorkflowJobQueryExecutor.WorkflowJobQuery; 052import org.apache.oozie.util.ConfigUtils; 053import org.apache.oozie.util.Instrumentation; 054import org.apache.oozie.util.XLog; 055 056/** 057 * The authorization service provides all authorization checks. 058 */ 059public class AuthorizationService implements Service { 060 061 public static final String CONF_PREFIX = Service.CONF_PREFIX + "AuthorizationService."; 062 063 /** 064 * Configuration parameter to enable or disable Oozie admin role. 065 */ 066 public static final String CONF_SECURITY_ENABLED = CONF_PREFIX + "security.enabled"; 067 068 /** 069 * Configuration parameter to enable or disable Oozie admin role. 070 */ 071 public static final String CONF_AUTHORIZATION_ENABLED = CONF_PREFIX + "authorization.enabled"; 072 073 /** 074 * Configuration parameter to enable old behavior default group as ACL. 075 */ 076 public static final String CONF_DEFAULT_GROUP_AS_ACL = CONF_PREFIX + "default.group.as.acl"; 077 078 /** 079 * Configuration parameter to define admin groups, if NULL/empty the adminusers.txt file is used. 080 */ 081 public static final String CONF_ADMIN_GROUPS = CONF_PREFIX + "admin.groups"; 082 083 public static final String CONF_SYSTEM_INFO_AUTHORIZED_USERS = CONF_PREFIX + "system.info.authorized.users"; 084 085 086 /** 087 * File that contains list of admin users for Oozie. 088 */ 089 public static final String ADMIN_USERS_FILE = "adminusers.txt"; 090 091 protected static final String INSTRUMENTATION_GROUP = "authorization"; 092 protected static final String INSTR_FAILED_AUTH_COUNTER = "authorization.failed"; 093 094 private Set<String> adminGroups; 095 private Set<String> adminUsers; 096 private Set<String> sysInfoAuthUsers; 097 private boolean authorizationEnabled; 098 private boolean useDefaultGroupAsAcl; 099 private boolean authorizedSystemInfo = false; 100 private final XLog log = XLog.getLog(getClass()); 101 private Instrumentation instrumentation; 102 103 private String[] getTrimmedStrings(String str) { 104 if (null == str || "".equals(str.trim())) { 105 return new String[0]; 106 } 107 return str.trim().split("\\s*,\\s*"); 108 } 109 110 /** 111 * Initialize the service. <p> Reads the security related configuration. parameters - security enabled and list of 112 * super users. 113 * 114 * @param services services instance. 115 * @throws ServiceException thrown if the service could not be initialized. 116 */ 117 public void init(Services services) throws ServiceException { 118 authorizationEnabled = 119 ConfigUtils.getWithDeprecatedCheck(services.getConf(), CONF_AUTHORIZATION_ENABLED, 120 CONF_SECURITY_ENABLED, false); 121 String systemInfoAuthUsers = ConfigurationService.get(CONF_SYSTEM_INFO_AUTHORIZED_USERS); 122 if (!StringUtils.isBlank(systemInfoAuthUsers)) { 123 authorizedSystemInfo = true; 124 sysInfoAuthUsers = new HashSet<>(); 125 for (String user : getTrimmedStrings(systemInfoAuthUsers)) { 126 sysInfoAuthUsers.add(user); 127 } 128 } 129 if (authorizationEnabled) { 130 log.info("Oozie running with authorization enabled"); 131 useDefaultGroupAsAcl = ConfigurationService.getBoolean(CONF_DEFAULT_GROUP_AS_ACL); 132 String[] str = getTrimmedStrings(Services.get().getConf().get(CONF_ADMIN_GROUPS)); 133 if (str.length > 0) { 134 log.info("Admin users will be checked against the defined admin groups"); 135 adminGroups = new HashSet<String>(); 136 for (String s : str) { 137 adminGroups.add(s.trim()); 138 } 139 } 140 else { 141 log.info("Admin users will be checked against the 'adminusers.txt' file contents"); 142 adminUsers = new HashSet<String>(); 143 loadAdminUsers(); 144 } 145 } 146 else { 147 log.warn("Oozie running with authorization disabled"); 148 } 149 instrumentation = Services.get().get(InstrumentationService.class).get(); 150 } 151 152 /** 153 * Return if security is enabled or not. 154 * 155 * @return if security is enabled or not. 156 */ 157 @Deprecated 158 public boolean isSecurityEnabled() { 159 return authorizationEnabled; 160 } 161 162 public boolean useDefaultGroupAsAcl() { 163 return useDefaultGroupAsAcl; 164 } 165 166 /** 167 * Return if security is enabled or not. 168 * 169 * @return if security is enabled or not. 170 */ 171 public boolean isAuthorizationEnabled() { 172 return isSecurityEnabled(); 173 } 174 175 /** 176 * Load the list of admin users from {@link AuthorizationService#ADMIN_USERS_FILE} </p> 177 * 178 * @throws ServiceException if the admin user list could not be loaded. 179 */ 180 private void loadAdminUsers() throws ServiceException { 181 String configDir = Services.get().get(ConfigurationService.class).getConfigDir(); 182 if (configDir != null) { 183 File file = new File(configDir, ADMIN_USERS_FILE); 184 if (file.exists()) { 185 try { 186 BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file))); 187 try { 188 String line = br.readLine(); 189 while (line != null) { 190 line = line.trim(); 191 if (line.length() > 0 && !line.startsWith("#")) { 192 adminUsers.add(line); 193 } 194 line = br.readLine(); 195 } 196 } 197 catch (IOException ex) { 198 throw new ServiceException(ErrorCode.E0160, file.getAbsolutePath(), ex); 199 } 200 } 201 catch (FileNotFoundException ex) { 202 throw new ServiceException(ErrorCode.E0160, file.getAbsolutePath(), ex); 203 } 204 } 205 else { 206 log.warn("Admin users file not available in config dir [{0}], running without admin users", configDir); 207 } 208 } 209 else { 210 log.warn("Reading configuration from classpath, running without admin users"); 211 } 212 } 213 214 /** 215 * Destroy the service. <p> This implementation does a NOP. 216 */ 217 public void destroy() { 218 } 219 220 /** 221 * Return the public interface of the service. 222 * 223 * @return {@link AuthorizationService}. 224 */ 225 public Class<? extends Service> getInterface() { 226 return AuthorizationService.class; 227 } 228 229 /** 230 * Check if the user belongs to the group or not. 231 * 232 * @param user user name. 233 * @param group group name. 234 * @return if the user belongs to the group or not. 235 * @throws AuthorizationException thrown if the authorization query can not be performed. 236 */ 237 protected boolean isUserInGroup(String user, String group) throws AuthorizationException { 238 GroupsService groupsService = Services.get().get(GroupsService.class); 239 try { 240 return groupsService.getGroups(user).contains(group); 241 } 242 catch (IOException ex) { 243 throw new AuthorizationException(ErrorCode.E0501, ex.getMessage(), ex); 244 } 245 } 246 247 /** 248 * Check if the user belongs to the group or not. <p> <p> Subclasses should override the {@link #isUserInGroup} 249 * method. 250 * 251 * @param user user name. 252 * @param group group name. 253 * @throws AuthorizationException thrown if the user is not authorized for the group or if the authorization query 254 * can not be performed. 255 */ 256 public void authorizeForGroup(String user, String group) throws AuthorizationException { 257 if (authorizationEnabled && !isUserInGroup(user, group)) { 258 throw new AuthorizationException(ErrorCode.E0502, user, group); 259 } 260 } 261 262 /** 263 * Return the default group to which the user belongs. <p> This implementation always returns 'users'. 264 * 265 * @param user user name. 266 * @return default group of user. 267 * @throws AuthorizationException thrown if the default group con not be retrieved. 268 */ 269 public String getDefaultGroup(String user) throws AuthorizationException { 270 try { 271 return Services.get().get(GroupsService.class).getGroups(user).get(0); 272 } 273 catch (IOException ex) { 274 throw new AuthorizationException(ErrorCode.E0501, ex.getMessage(), ex); 275 } 276 } 277 278 /** 279 * Check if the user has admin privileges. <p> If admin is disabled it returns always <code>true</code>. <p> If 280 * admin is enabled it returns <code>true</code> if the user is in the <code>adminusers.txt</code> file. 281 * 282 * @param user user name. 283 * @return if the user has admin privileges or not. 284 */ 285 protected boolean isAdmin(String user) { 286 boolean admin = false; 287 if (adminUsers != null) { 288 admin = adminUsers.contains(user); 289 } 290 else { 291 for (String adminGroup : adminGroups) { 292 try { 293 admin = isUserInGroup(user, adminGroup); 294 if (admin) { 295 break; 296 } 297 } 298 catch (AuthorizationException ex) { 299 log.warn("Admin check failed, " + ex.toString(), ex); 300 break; 301 } 302 } 303 } 304 return admin; 305 } 306 307 /** 308 * Check if the user is authorized to access system information. 309 * 310 * @param user user name. 311 * @param proxyUser proxy user name. 312 * @throws AuthorizationException thrown if user does not have admin priviledges. 313 */ 314 public void authorizeForSystemInfo(String user, String proxyUser) throws AuthorizationException { 315 if (authorizationEnabled && authorizedSystemInfo && !(sysInfoAuthUsers.contains(user) || sysInfoAuthUsers 316 .contains(proxyUser) || isAdmin(user) || isAdmin(proxyUser))) { 317 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 318 throw new AuthorizationException(ErrorCode.E0503, user); 319 } 320 } 321 322 /** 323 * Check if the user has admin privileges. <p> Subclasses should override the {@link #isUserInGroup} method. 324 * 325 * @param user user name. 326 * @param write indicates if the check is for read or write admin tasks (in this implementation this is ignored) 327 * @throws AuthorizationException thrown if user does not have admin privileges. 328 */ 329 public void authorizeForAdmin(String user, boolean write) throws AuthorizationException { 330 if (authorizationEnabled && write && !isAdmin(user)) { 331 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 332 throw new AuthorizationException(ErrorCode.E0503, user); 333 } 334 } 335 336 /** 337 * Check if the user+group is authorized to use the specified application. <p> The check is done by checking the 338 * file system permissions on the workflow application. 339 * 340 * @param user user name. 341 * @param group group name. 342 * @param appPath application path. 343 * @throws AuthorizationException thrown if the user is not authorized for the app. 344 */ 345 public void authorizeForApp(String user, String group, String appPath, Configuration jobConf) 346 throws AuthorizationException { 347 try { 348 HadoopAccessorService has = Services.get().get(HadoopAccessorService.class); 349 URI uri = new Path(appPath).toUri(); 350 Configuration fsConf = has.createConfiguration(uri.getAuthority()); 351 FileSystem fs = has.createFileSystem(user, uri, fsConf); 352 353 Path path = new Path(appPath); 354 try { 355 if (!fs.exists(path)) { 356 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 357 throw new AuthorizationException(ErrorCode.E0504, appPath); 358 } 359 Path wfXml = new Path(path, "workflow.xml"); 360 if (!fs.exists(wfXml)) { 361 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 362 throw new AuthorizationException(ErrorCode.E0505, appPath); 363 } 364 if (!fs.isFile(wfXml)) { 365 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 366 throw new AuthorizationException(ErrorCode.E0506, appPath); 367 } 368 fs.open(wfXml).close(); 369 } 370 catch (AccessControlException ex) { 371 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 372 throw new AuthorizationException(ErrorCode.E0507, appPath, ex.getMessage(), ex); 373 } 374 } 375 catch (IOException ex) { 376 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 377 throw new AuthorizationException(ErrorCode.E0501, ex.getMessage(), ex); 378 } 379 catch (HadoopAccessorException e) { 380 throw new AuthorizationException(e); 381 } 382 } 383 384 /** 385 * Check if the user+group is authorized to use the specified application. <p> The check is done by checking the 386 * file system permissions on the workflow application. 387 * 388 * @param user user name. 389 * @param group group name. 390 * @param appPath application path. 391 * @param fileName workflow or coordinator.xml 392 * @param conf 393 * @throws AuthorizationException thrown if the user is not authorized for the app. 394 */ 395 public void authorizeForApp(String user, String group, String appPath, String fileName, Configuration conf) 396 throws AuthorizationException { 397 try { 398 HadoopAccessorService has = Services.get().get(HadoopAccessorService.class); 399 URI uri = new Path(appPath).toUri(); 400 Configuration fsConf = has.createConfiguration(uri.getAuthority()); 401 FileSystem fs = has.createFileSystem(user, uri, fsConf); 402 403 Path path = new Path(appPath); 404 try { 405 if (!fs.exists(path)) { 406 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 407 throw new AuthorizationException(ErrorCode.E0504, appPath); 408 } 409 if (conf.get(XOozieClient.IS_PROXY_SUBMISSION) == null) { // Only further check existence of job definition 410 //files for non proxy submission jobs; 411 if (!fs.isFile(path)) { 412 Path appXml = new Path(path, fileName); 413 if (!fs.exists(appXml)) { 414 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 415 throw new AuthorizationException(ErrorCode.E0505, appPath); 416 } 417 if (!fs.isFile(appXml)) { 418 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 419 throw new AuthorizationException(ErrorCode.E0506, appPath); 420 } 421 fs.open(appXml).close(); 422 } 423 } 424 } 425 catch (AccessControlException ex) { 426 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 427 throw new AuthorizationException(ErrorCode.E0507, appPath, ex.getMessage(), ex); 428 } 429 } 430 catch (IOException ex) { 431 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 432 throw new AuthorizationException(ErrorCode.E0501, ex.getMessage(), ex); 433 } 434 catch (HadoopAccessorException e) { 435 throw new AuthorizationException(e); 436 } 437 } 438 439 private boolean isUserInAcl(String user, String aclStr) throws IOException { 440 boolean userInAcl = false; 441 if (aclStr != null && aclStr.trim().length() > 0) { 442 GroupsService groupsService = Services.get().get(GroupsService.class); 443 String[] acl = aclStr.split(","); 444 for (int i = 0; !userInAcl && i < acl.length; i++) { 445 String aclItem = acl[i].trim(); 446 userInAcl = aclItem.equals(user) || groupsService.getGroups(user).contains(aclItem); 447 } 448 } 449 return userInAcl; 450 } 451 452 /** 453 * Check if the user+group is authorized to operate on the specified job. <p> Checks if the user is a super-user or 454 * the one who started the job. <p> Read operations are allowed to all users. 455 * 456 * @param user user name. 457 * @param jobId job id. 458 * @param write indicates if the check is for read or write job tasks. 459 * @throws AuthorizationException thrown if the user is not authorized for the job. 460 */ 461 public void authorizeForJob(String user, String jobId, boolean write) throws AuthorizationException { 462 if (authorizationEnabled && write && !isAdmin(user)) { 463 try { 464 // handle workflow jobs 465 if (jobId.endsWith("-W")) { 466 WorkflowJobBean jobBean = null; 467 JPAService jpaService = Services.get().get(JPAService.class); 468 if (jpaService != null) { 469 try { 470 jobBean = WorkflowJobQueryExecutor.getInstance().get(WorkflowJobQuery.GET_WORKFLOW_USER_GROUP, jobId); 471 } 472 catch (JPAExecutorException je) { 473 throw new AuthorizationException(je); 474 } 475 } 476 else { 477 throw new AuthorizationException(ErrorCode.E0610); 478 } 479 if (jobBean != null && !jobBean.getUser().equals(user)) { 480 if (!isUserInAcl(user, jobBean.getGroup())) { 481 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 482 throw new AuthorizationException(ErrorCode.E0508, user, jobId); 483 } 484 } 485 } 486 // handle bundle jobs 487 else if (jobId.endsWith("-B")){ 488 BundleJobBean jobBean = null; 489 JPAService jpaService = Services.get().get(JPAService.class); 490 if (jpaService != null) { 491 try { 492 jobBean = jpaService.execute(new BundleJobGetJPAExecutor(jobId)); 493 } 494 catch (JPAExecutorException je) { 495 throw new AuthorizationException(je); 496 } 497 } 498 else { 499 throw new AuthorizationException(ErrorCode.E0610); 500 } 501 if (jobBean != null && !jobBean.getUser().equals(user)) { 502 if (!isUserInAcl(user, jobBean.getGroup())) { 503 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 504 throw new AuthorizationException(ErrorCode.E0509, user, jobId); 505 } 506 } 507 } 508 // handle coordinator jobs 509 else { 510 CoordinatorJobBean jobBean = null; 511 JPAService jpaService = Services.get().get(JPAService.class); 512 if (jpaService != null) { 513 try { 514 jobBean = jpaService.execute(new CoordJobGetJPAExecutor(jobId)); 515 } 516 catch (JPAExecutorException je) { 517 throw new AuthorizationException(je); 518 } 519 } 520 else { 521 throw new AuthorizationException(ErrorCode.E0610); 522 } 523 if (jobBean != null && !jobBean.getUser().equals(user)) { 524 if (!isUserInAcl(user, jobBean.getGroup())) { 525 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 526 throw new AuthorizationException(ErrorCode.E0509, user, jobId); 527 } 528 } 529 } 530 } 531 catch (IOException ex) { 532 throw new AuthorizationException(ErrorCode.E0501, ex.getMessage(), ex); 533 } 534 } 535 } 536 537 /** 538 * Check if the user+group is authorized to operate on the specified jobs. <p> Checks if the user is a super-user or 539 * the one who started the jobs. <p> Read operations are allowed to all users. 540 * 541 * @param user user name. 542 * @param filter filter used to select jobs 543 * @param start starting index of the jobs in DB 544 * @param len maximum amount of jobs to select 545 * @param write indicates if the check is for read or write job tasks. 546 * @throws AuthorizationException thrown if the user is not authorized for the job. 547 */ 548 public void authorizeForJobs(String user, Map<String, List<String>> filter, String jobType, 549 int start, int len, boolean write) throws AuthorizationException { 550 if (authorizationEnabled && write && !isAdmin(user)) { 551 try { 552 // handle workflow jobs 553 if (jobType.equals("wf")) { 554 List<WorkflowJobBean> jobBeans = new ArrayList<WorkflowJobBean>(); 555 JPAService jpaService = Services.get().get(JPAService.class); 556 if (jpaService != null) { 557 try { 558 jobBeans = jpaService.execute(new WorkflowsJobGetJPAExecutor( 559 filter, start, len)).getWorkflows(); 560 } 561 catch (JPAExecutorException je) { 562 throw new AuthorizationException(je); 563 } 564 } 565 else { 566 throw new AuthorizationException(ErrorCode.E0610); 567 } 568 for (WorkflowJobBean jobBean : jobBeans) { 569 if (jobBean != null && !jobBean.getUser().equals(user)) { 570 if (!isUserInAcl(user, jobBean.getGroup())) { 571 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 572 throw new AuthorizationException(ErrorCode.E0508, user, jobBean.getId()); 573 } 574 } 575 } 576 } 577 // handle bundle jobs 578 else if (jobType.equals("bundle")) { 579 List<BundleJobBean> jobBeans = new ArrayList<BundleJobBean>(); 580 JPAService jpaService = Services.get().get(JPAService.class); 581 if (jpaService != null) { 582 try { 583 jobBeans = jpaService.execute(new BundleJobInfoGetJPAExecutor( 584 filter, start, len)).getBundleJobs(); 585 } 586 catch (JPAExecutorException je) { 587 throw new AuthorizationException(je); 588 } 589 } 590 else { 591 throw new AuthorizationException(ErrorCode.E0610); 592 } 593 for (BundleJobBean jobBean : jobBeans){ 594 if (jobBean != null && !jobBean.getUser().equals(user)) { 595 if (!isUserInAcl(user, jobBean.getGroup())) { 596 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 597 throw new AuthorizationException(ErrorCode.E0509, user, jobBean.getId()); 598 } 599 } 600 } 601 } 602 // handle coordinator jobs 603 else { 604 List<CoordinatorJobBean> jobBeans = new ArrayList<CoordinatorJobBean>(); 605 JPAService jpaService = Services.get().get(JPAService.class); 606 if (jpaService != null) { 607 try { 608 jobBeans = jpaService.execute(new CoordJobInfoGetJPAExecutor( 609 filter, start, len)).getCoordJobs(); 610 } 611 catch (JPAExecutorException je) { 612 throw new AuthorizationException(je); 613 } 614 } 615 else { 616 throw new AuthorizationException(ErrorCode.E0610); 617 } 618 for (CoordinatorJobBean jobBean : jobBeans) { 619 if (jobBean != null && !jobBean.getUser().equals(user)) { 620 if (!isUserInAcl(user, jobBean.getGroup())) { 621 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 622 throw new AuthorizationException(ErrorCode.E0509, user, jobBean.getId()); 623 } 624 } 625 } 626 } 627 } 628 catch (IOException ex) { 629 throw new AuthorizationException(ErrorCode.E0501, ex.getMessage(), ex); 630 } 631 } 632 } 633 /** 634 * Convenience method for instrumentation counters. 635 * 636 * @param name counter name. 637 * @param count count to increment the counter. 638 */ 639 private void incrCounter(String name, int count) { 640 if (instrumentation != null) { 641 instrumentation.incr(INSTRUMENTATION_GROUP, name, count); 642 } 643 } 644 645 public boolean isAuthorizedSystemInfo() { 646 return authorizedSystemInfo; 647 } 648}