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    }