001 /** 002 * Copyright (c) 2010 Yahoo! Inc. All rights reserved. 003 * Licensed under the Apache License, Version 2.0 (the "License"); 004 * you may not use this file except in compliance with the License. 005 * You may obtain a copy of the License at 006 * 007 * http://www.apache.org/licenses/LICENSE-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, software 010 * distributed under the License is distributed on an "AS IS" BASIS, 011 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012 * See the License for the specific language governing permissions and 013 * limitations under the License. See accompanying LICENSE file. 014 */ 015 package org.apache.oozie.service; 016 017 import java.io.BufferedReader; 018 import java.io.File; 019 import java.io.FileInputStream; 020 import java.io.FileNotFoundException; 021 import java.io.IOException; 022 import java.io.InputStreamReader; 023 import java.util.HashSet; 024 import java.util.Set; 025 026 import org.apache.hadoop.conf.Configuration; 027 import org.apache.hadoop.fs.FileSystem; 028 import org.apache.hadoop.fs.Path; 029 import org.apache.oozie.CoordinatorJobBean; 030 import org.apache.oozie.ErrorCode; 031 import org.apache.oozie.WorkflowJobBean; 032 import org.apache.oozie.client.XOozieClient; 033 import org.apache.oozie.store.CoordinatorStore; 034 import org.apache.oozie.store.StoreException; 035 import org.apache.oozie.store.WorkflowStore; 036 import org.apache.oozie.util.Instrumentation; 037 import org.apache.oozie.util.XLog; 038 039 /** 040 * The authorization service provides all authorization checks. 041 */ 042 public class AuthorizationService implements Service { 043 044 public static final String CONF_PREFIX = Service.CONF_PREFIX + "AuthorizationService."; 045 046 /** 047 * Configuration parameter to enable or disable Oozie admin role. 048 */ 049 public static final String CONF_SECURITY_ENABLED = CONF_PREFIX + "security.enabled"; 050 051 /** 052 * File that contains list of admin users for Oozie. 053 */ 054 public static final String ADMIN_USERS_FILE = "adminusers.txt"; 055 056 /** 057 * Default group returned by getDefaultGroup(). 058 */ 059 public static final String DEFAULT_GROUP = "users"; 060 061 protected static final String INSTRUMENTATION_GROUP = "authorization"; 062 protected static final String INSTR_FAILED_AUTH_COUNTER = "authorization.failed"; 063 064 private Set<String> adminUsers; 065 private boolean securityEnabled; 066 067 private final XLog log = XLog.getLog(getClass()); 068 private Instrumentation instrumentation; 069 070 /** 071 * Initialize the service. <p/> Reads the security related configuration. parameters - security enabled and list of 072 * super users. 073 * 074 * @param services services instance. 075 * @throws ServiceException thrown if the service could not be initialized. 076 */ 077 public void init(Services services) throws ServiceException { 078 adminUsers = new HashSet<String>(); 079 securityEnabled = services.getConf().getBoolean(CONF_SECURITY_ENABLED, false); 080 instrumentation = Services.get().get(InstrumentationService.class).get(); 081 if (securityEnabled) { 082 log.info("Oozie running with security enabled"); 083 loadAdminUsers(); 084 } 085 else { 086 log.warn("Oozie running with security disabled"); 087 } 088 } 089 090 /** 091 * Return if security is enabled or not. 092 * 093 * @return if security is enabled or not. 094 */ 095 public boolean isSecurityEnabled() { 096 return securityEnabled; 097 } 098 099 /** 100 * Load the list of admin users from {@link AuthorizationService#ADMIN_USERS_FILE} </p> 101 * 102 * @throws ServiceException if the admin user list could not be loaded. 103 */ 104 private void loadAdminUsers() throws ServiceException { 105 String configDir = Services.get().get(ConfigurationService.class).getConfigDir(); 106 if (configDir != null) { 107 File file = new File(configDir, ADMIN_USERS_FILE); 108 if (file.exists()) { 109 try { 110 BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file))); 111 try { 112 String line = br.readLine(); 113 while (line != null) { 114 line = line.trim(); 115 if (line.length() > 0 && !line.startsWith("#")) { 116 adminUsers.add(line); 117 } 118 line = br.readLine(); 119 } 120 } 121 catch (IOException ex) { 122 throw new ServiceException(ErrorCode.E0160, file.getAbsolutePath(), ex); 123 } 124 } 125 catch (FileNotFoundException ex) { 126 throw new ServiceException(ErrorCode.E0160, ex); 127 } 128 } 129 else { 130 log.warn("Admin users file not available in config dir [{0}], running without admin users", configDir); 131 } 132 } 133 else { 134 log.warn("Reading configuration from classpath, running without admin users"); 135 } 136 } 137 138 /** 139 * Destroy the service. <p/> This implementation does a NOP. 140 */ 141 public void destroy() { 142 } 143 144 /** 145 * Return the public interface of the service. 146 * 147 * @return {@link AuthorizationService}. 148 */ 149 public Class<? extends Service> getInterface() { 150 return AuthorizationService.class; 151 } 152 153 /** 154 * Check if the user belongs to the group or not. <p/> This implementation returns always <code>true</code>. 155 * 156 * @param user user name. 157 * @param group group name. 158 * @return if the user belongs to the group or not. 159 * @throws AuthorizationException thrown if the authorization query can not be performed. 160 */ 161 protected boolean isUserInGroup(String user, String group) throws AuthorizationException { 162 return true; 163 } 164 165 /** 166 * Check if the user belongs to the group or not. <p/> <p/> Subclasses should override the {@link #isUserInGroup} 167 * method. 168 * 169 * @param user user name. 170 * @param group group name. 171 * @throws AuthorizationException thrown if the user is not authorized for the group or if the authorization query 172 * can not be performed. 173 */ 174 public void authorizeForGroup(String user, String group) throws AuthorizationException { 175 if (securityEnabled && !isUserInGroup(user, group)) { 176 throw new AuthorizationException(ErrorCode.E0502, user, group); 177 } 178 } 179 180 /** 181 * Return the default group to which the user belongs. <p/> This implementation always returns 'users'. 182 * 183 * @param user user name. 184 * @return default group of user. 185 * @throws AuthorizationException thrown if the default group con not be retrieved. 186 */ 187 public String getDefaultGroup(String user) throws AuthorizationException { 188 return DEFAULT_GROUP; 189 } 190 191 /** 192 * Check if the user has admin privileges. <p/> If admin is disabled it returns always <code>true</code>. <p/> If 193 * admin is enabled it returns <code>true</code> if the user is in the <code>adminusers.txt</code> file. 194 * 195 * @param user user name. 196 * @return if the user has admin privileges or not. 197 */ 198 protected boolean isAdmin(String user) { 199 return adminUsers.contains(user); 200 } 201 202 /** 203 * Check if the user has admin privileges. <p/> Subclasses should override the {@link #isUserInGroup} method. 204 * 205 * @param user user name. 206 * @param write indicates if the check is for read or write admin tasks (in this implementation this is ignored) 207 * @throws AuthorizationException thrown if user does not have admin priviledges. 208 */ 209 public void authorizeForAdmin(String user, boolean write) throws AuthorizationException { 210 if (securityEnabled && write && !isAdmin(user)) { 211 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 212 throw new AuthorizationException(ErrorCode.E0503, user); 213 } 214 } 215 216 /** 217 * Check if the user+group is authorized to use the specified application. <p/> The check is done by checking the 218 * file system permissions on the workflow application. 219 * 220 * @param user user name. 221 * @param group group name. 222 * @param appPath application path. 223 * @throws AuthorizationException thrown if the user is not authorized for the app. 224 */ 225 public void authorizeForApp(String user, String group, String appPath, Configuration jobConf) 226 throws AuthorizationException { 227 try { 228 FileSystem fs = Services.get().get(HadoopAccessorService.class).createFileSystem(user, group, 229 new Path(appPath).toUri(), jobConf); 230 231 Path path = new Path(appPath); 232 try { 233 if (!fs.exists(path)) { 234 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 235 throw new AuthorizationException(ErrorCode.E0504, appPath); 236 } 237 Path wfXml = new Path(path, "workflow.xml"); 238 if (!fs.exists(wfXml)) { 239 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 240 throw new AuthorizationException(ErrorCode.E0505, appPath); 241 } 242 if (!fs.isFile(wfXml)) { 243 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 244 throw new AuthorizationException(ErrorCode.E0506, appPath); 245 } 246 fs.open(wfXml).close(); 247 } 248 // TODO change this when stopping support of 0.18 to the new 249 // Exception 250 catch (org.apache.hadoop.fs.permission.AccessControlException ex) { 251 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 252 throw new AuthorizationException(ErrorCode.E0507, appPath, ex.getMessage(), ex); 253 } 254 } 255 catch (IOException ex) { 256 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 257 throw new AuthorizationException(ErrorCode.E0501, ex.getMessage(), ex); 258 } 259 catch (HadoopAccessorException e) { 260 throw new AuthorizationException(e); 261 } 262 } 263 264 /** 265 * Check if the user+group is authorized to use the specified application. <p/> The check is done by checking the 266 * file system permissions on the workflow application. 267 * 268 * @param user user name. 269 * @param group group name. 270 * @param appPath application path. 271 * @param fileName workflow or coordinator.xml 272 * @param conf 273 * @throws AuthorizationException thrown if the user is not authorized for the app. 274 */ 275 public void authorizeForApp(String user, String group, String appPath, String fileName, Configuration conf) 276 throws AuthorizationException { 277 try { 278 //Configuration conf = new Configuration(); 279 //conf.set("user.name", user); 280 // TODO Temporary fix till 281 // https://issues.apache.org/jira/browse/HADOOP-4875 is resolved. 282 //conf.set("hadoop.job.ugi", user + "," + group); 283 FileSystem fs = Services.get().get(HadoopAccessorService.class).createFileSystem(user, group, 284 new Path(appPath).toUri(), conf); 285 Path path = new Path(appPath); 286 try { 287 if (!fs.exists(path)) { 288 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 289 throw new AuthorizationException(ErrorCode.E0504, appPath); 290 } 291 if (conf.get(XOozieClient.IS_PROXY_SUBMISSION) == null) { // Only further check existence of job definition files for non proxy submission jobs; 292 if (!fs.isFile(path)) { 293 Path appXml = new Path(path, fileName); 294 if (!fs.exists(appXml)) { 295 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 296 throw new AuthorizationException(ErrorCode.E0505, appPath); 297 } 298 if (!fs.isFile(appXml)) { 299 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 300 throw new AuthorizationException(ErrorCode.E0506, appPath); 301 } 302 fs.open(appXml).close(); 303 } 304 } 305 } 306 // TODO change this when stopping support of 0.18 to the new 307 // Exception 308 catch (org.apache.hadoop.fs.permission.AccessControlException ex) { 309 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 310 throw new AuthorizationException(ErrorCode.E0507, appPath, ex.getMessage(), ex); 311 } 312 } 313 catch (IOException ex) { 314 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 315 throw new AuthorizationException(ErrorCode.E0501, ex.getMessage(), ex); 316 } 317 catch (HadoopAccessorException e) { 318 throw new AuthorizationException(e); 319 } 320 } 321 322 /** 323 * Check if the user+group is authorized to operate on the specified job. <p/> Checks if the user is a super-user or 324 * the one who started the job. <p/> Read operations are allowed to all users. 325 * 326 * @param user user name. 327 * @param jobId job id. 328 * @param write indicates if the check is for read or write job tasks. 329 * @throws AuthorizationException thrown if the user is not authorized for the job. 330 */ 331 public void authorizeForJob(String user, String jobId, boolean write) throws AuthorizationException { 332 if (securityEnabled && write && !isAdmin(user)) { 333 // handle workflow jobs 334 if (jobId.endsWith("-W")) { 335 WorkflowJobBean jobBean = null; 336 WorkflowStore store = null; 337 try { 338 store = Services.get().get(WorkflowStoreService.class).create(); 339 store.beginTrx(); 340 jobBean = store.getWorkflow(jobId, false); 341 store.commitTrx(); 342 } 343 catch (StoreException ex) { 344 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 345 if (store != null) { 346 store.rollbackTrx(); 347 } 348 throw new AuthorizationException(ex); 349 } 350 catch (Exception ex) { 351 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 352 log.error("Exception, {0}", ex.getMessage(), ex); 353 if (store != null && store.isActive()) { 354 try { 355 store.rollbackTrx(); 356 } 357 catch (RuntimeException rex) { 358 log.warn("openjpa error, {0}", rex.getMessage(), rex); 359 } 360 } 361 throw new AuthorizationException(ErrorCode.E0501, ex); 362 } 363 finally { 364 if (store != null) { 365 if (!store.isActive()) { 366 try { 367 store.closeTrx(); 368 } 369 catch (RuntimeException rex) { 370 log.warn("Exception while attempting to close store", rex); 371 } 372 } 373 else { 374 log.warn("transaction is not committed or rolled back before closing entitymanager."); 375 } 376 } 377 } 378 if (jobBean != null && !jobBean.getUser().equals(user)) { 379 if (!isUserInGroup(user, jobBean.getGroup())) { 380 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 381 throw new AuthorizationException(ErrorCode.E0508, user, jobId); 382 } 383 } 384 } 385 // handle coordinator jobs 386 else { 387 CoordinatorJobBean jobBean = null; 388 CoordinatorStore store = null; 389 try { 390 store = Services.get().get(CoordinatorStoreService.class).create(); 391 store.beginTrx(); 392 jobBean = store.getCoordinatorJob(jobId, false); 393 store.commitTrx(); 394 } 395 catch (StoreException ex) { 396 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 397 if (store != null) { 398 store.rollbackTrx(); 399 } 400 throw new AuthorizationException(ex); 401 } 402 catch (Exception ex) { 403 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 404 log.error("Exception, {0}", ex.getMessage(), ex); 405 if (store != null && store.isActive()) { 406 try { 407 store.rollbackTrx(); 408 } 409 catch (RuntimeException rex) { 410 log.warn("openjpa error, {0}", rex.getMessage(), rex); 411 } 412 } 413 throw new AuthorizationException(ErrorCode.E0501, ex); 414 } 415 finally { 416 if (store != null) { 417 if (!store.isActive()) { 418 try { 419 store.closeTrx(); 420 } 421 catch (RuntimeException rex) { 422 log.warn("Exception while attempting to close store", rex); 423 } 424 } 425 else { 426 log.warn("transaction is not committed or rolled back before closing entitymanager."); 427 } 428 } 429 } 430 if (jobBean != null && !jobBean.getUser().equals(user)) { 431 if (!isUserInGroup(user, jobBean.getGroup())) { 432 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 433 throw new AuthorizationException(ErrorCode.E0509, user, jobId); 434 } 435 } 436 } 437 } 438 } 439 440 /** 441 * Convenience method for instrumentation counters. 442 * 443 * @param name counter name. 444 * @param count count to increment the counter. 445 */ 446 private void incrCounter(String name, int count) { 447 if (instrumentation != null) { 448 instrumentation.incr(INSTRUMENTATION_GROUP, name, count); 449 } 450 } 451 }