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 = ConfigurationService.getConfigurationDirectory(); 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.LIBPATH) == null) { // Only check existance of wfXml for non http submission jobs; 292 Path wfXml = new Path(path, fileName); 293 if (!fs.exists(wfXml)) { 294 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 295 throw new AuthorizationException(ErrorCode.E0505, appPath); 296 } 297 if (!fs.isFile(wfXml)) { 298 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 299 throw new AuthorizationException(ErrorCode.E0506, appPath); 300 } 301 fs.open(wfXml).close(); 302 } 303 } 304 // TODO change this when stopping support of 0.18 to the new 305 // Exception 306 catch (org.apache.hadoop.fs.permission.AccessControlException ex) { 307 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 308 throw new AuthorizationException(ErrorCode.E0507, appPath, ex.getMessage(), ex); 309 } 310 } 311 catch (IOException ex) { 312 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 313 throw new AuthorizationException(ErrorCode.E0501, ex.getMessage(), ex); 314 } 315 catch (HadoopAccessorException e) { 316 throw new AuthorizationException(e); 317 } 318 } 319 320 /** 321 * Check if the user+group is authorized to operate on the specified job. <p/> Checks if the user is a super-user or 322 * the one who started the job. <p/> Read operations are allowed to all users. 323 * 324 * @param user user name. 325 * @param jobId job id. 326 * @param write indicates if the check is for read or write job tasks. 327 * @throws AuthorizationException thrown if the user is not authorized for the job. 328 */ 329 public void authorizeForJob(String user, String jobId, boolean write) throws AuthorizationException { 330 if (securityEnabled && write && !isAdmin(user)) { 331 // handle workflow jobs 332 if (jobId.endsWith("-W")) { 333 WorkflowJobBean jobBean = null; 334 WorkflowStore store = null; 335 try { 336 store = Services.get().get(WorkflowStoreService.class).create(); 337 store.beginTrx(); 338 jobBean = store.getWorkflow(jobId, false); 339 store.commitTrx(); 340 } 341 catch (StoreException ex) { 342 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 343 if (store != null) { 344 store.rollbackTrx(); 345 } 346 throw new AuthorizationException(ex); 347 } 348 catch (Exception ex) { 349 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 350 log.error("Exception, {0}", ex.getMessage(), ex); 351 if (store != null && store.isActive()) { 352 try { 353 store.rollbackTrx(); 354 } 355 catch (RuntimeException rex) { 356 log.warn("openjpa error, {0}", rex.getMessage(), rex); 357 } 358 } 359 throw new AuthorizationException(ErrorCode.E0501, ex); 360 } 361 finally { 362 if (store != null) { 363 if (!store.isActive()) { 364 try { 365 store.closeTrx(); 366 } 367 catch (RuntimeException rex) { 368 log.warn("Exception while attempting to close store", rex); 369 } 370 } 371 else { 372 log.warn("transaction is not committed or rolled back before closing entitymanager."); 373 } 374 } 375 } 376 if (jobBean != null && !jobBean.getUser().equals(user)) { 377 if (!isUserInGroup(user, jobBean.getGroup())) { 378 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 379 throw new AuthorizationException(ErrorCode.E0508, user, jobId); 380 } 381 } 382 } 383 // handle coordinator jobs 384 else { 385 CoordinatorJobBean jobBean = null; 386 CoordinatorStore store = null; 387 try { 388 store = Services.get().get(CoordinatorStoreService.class).create(); 389 store.beginTrx(); 390 jobBean = store.getCoordinatorJob(jobId, false); 391 store.commitTrx(); 392 } 393 catch (StoreException ex) { 394 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 395 if (store != null) { 396 store.rollbackTrx(); 397 } 398 throw new AuthorizationException(ex); 399 } 400 catch (Exception ex) { 401 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 402 log.error("Exception, {0}", ex.getMessage(), ex); 403 if (store != null && store.isActive()) { 404 try { 405 store.rollbackTrx(); 406 } 407 catch (RuntimeException rex) { 408 log.warn("openjpa error, {0}", rex.getMessage(), rex); 409 } 410 } 411 throw new AuthorizationException(ErrorCode.E0501, ex); 412 } 413 finally { 414 if (store != null) { 415 if (!store.isActive()) { 416 try { 417 store.closeTrx(); 418 } 419 catch (RuntimeException rex) { 420 log.warn("Exception while attempting to close store", rex); 421 } 422 } 423 else { 424 log.warn("transaction is not committed or rolled back before closing entitymanager."); 425 } 426 } 427 } 428 if (jobBean != null && !jobBean.getUser().equals(user)) { 429 if (!isUserInGroup(user, jobBean.getGroup())) { 430 incrCounter(INSTR_FAILED_AUTH_COUNTER, 1); 431 throw new AuthorizationException(ErrorCode.E0509, user, jobId); 432 } 433 } 434 } 435 } 436 } 437 438 /** 439 * Convenience method for instrumentation counters. 440 * 441 * @param name counter name. 442 * @param count count to increment the counter. 443 */ 444 private void incrCounter(String name, int count) { 445 if (instrumentation != null) { 446 instrumentation.incr(INSTRUMENTATION_GROUP, name, count); 447 } 448 } 449 }