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