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 }