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    }