001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *      http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    package org.apache.oozie.service;
019    
020    import org.apache.oozie.ErrorCode;
021    import org.apache.oozie.util.ParamChecker;
022    import org.apache.oozie.util.XLog;
023    
024    import java.io.IOException;
025    import java.net.InetAddress;
026    import java.security.AccessControlException;
027    import java.text.MessageFormat;
028    import java.util.Arrays;
029    import java.util.HashMap;
030    import java.util.HashSet;
031    import java.util.List;
032    import java.util.Map;
033    import java.util.Set;
034    
035    /**
036     * The ProxyUserService checks if a user of a request has proxyuser privileges.
037     * <p/>
038     * This check is based on the following criteria:
039     * <p/>
040     * <ul>
041     *     <li>The user of the request must be configured as proxy user in Oozie configuration.</li>
042     *     <li>The user of the request must be making the request from a whitelisted host.</li>
043     *     <li>The user of the request must be making the request on behalf of a user of a whitelisted group.</li>
044     * </ul>
045     * <p/>
046     */
047    public class ProxyUserService implements Service {
048        private static XLog LOG = XLog.getLog(ProxyUserService.class);
049    
050        public static final String CONF_PREFIX = Service.CONF_PREFIX + "ProxyUserService.proxyuser.";
051        public static final String GROUPS = ".groups";
052        public static final String HOSTS = ".hosts";
053    
054        private Services services;
055        private Map<String, Set<String>> proxyUserHosts = new HashMap<String, Set<String>>();
056        private Map<String, Set<String>> proxyUserGroups = new HashMap<String, Set<String>>();
057    
058        /**
059         * Returns the service interface.
060         * @return <code>ProxyUserService</code>
061         */
062        @Override
063        public Class<? extends Service> getInterface() {
064            return ProxyUserService.class;
065        }
066    
067        /**
068         * Initializes the service.
069         *
070         * @param services services singleton initializing the service.
071         * @throws ServiceException thrown if the service could not be configured correctly.
072         */
073        @Override
074        public void init(Services services) throws ServiceException {
075            this.services = services;
076            for (Map.Entry<String, String> entry : services.getConf()) {
077                String key = entry.getKey();
078                if (key.startsWith(CONF_PREFIX) && key.endsWith(GROUPS)) {
079                    String proxyUser = key.substring(0, key.lastIndexOf(GROUPS));
080                    if (services.getConf().get(proxyUser + HOSTS) == null) {
081                        throw new ServiceException(ErrorCode.E0551, CONF_PREFIX + proxyUser + HOSTS);
082                    }
083                    proxyUser = proxyUser.substring(CONF_PREFIX.length());
084                    String value = entry.getValue().trim();
085                    LOG.info("Loading proxyuser settings [{0}]=[{1}]", key, value);
086                    Set<String> values = null;
087                    if (!value.equals("*")) {
088                        values = new HashSet<String>(Arrays.asList(value.split(",")));
089                    }
090                    proxyUserGroups.put(proxyUser, values);
091                }
092                if (key.startsWith(CONF_PREFIX) && key.endsWith(HOSTS)) {
093                    String proxyUser = key.substring(0, key.lastIndexOf(HOSTS));
094                    if (services.getConf().get(proxyUser + GROUPS) == null) {
095                        throw new ServiceException(ErrorCode.E0551, CONF_PREFIX + proxyUser + GROUPS);
096                    }
097                    proxyUser = proxyUser.substring(CONF_PREFIX.length());
098                    String value = entry.getValue().trim();
099                    LOG.info("Loading proxyuser settings [{0}]=[{1}]", key, value);
100                    Set<String> values = null;
101                    if (!value.equals("*")) {
102                        String[] hosts = value.split(",");
103                        for (int i = 0; i < hosts.length; i++) {
104                            String originalName = hosts[i];
105                            try {
106                                hosts[i] = normalizeHostname(originalName);
107                            }
108                            catch (Exception ex) {
109                                throw new ServiceException(ErrorCode.E0550, originalName, ex.getMessage(), ex);
110                            }
111                            LOG.info("  Hostname, original [{0}], normalized [{1}]", originalName, hosts[i]);
112                        }
113                        values = new HashSet<String>(Arrays.asList(hosts));
114                    }
115                    proxyUserHosts.put(proxyUser, values);
116                }
117            }
118        }
119    
120        /**
121         * Verifies a proxyuser.
122         *
123         * @param proxyUser user name of the proxy user.
124         * @param proxyHost host the proxy user is making the request from.
125         * @param doAsUser user the proxy user is impersonating.
126         * @throws IOException thrown if an error during the validation has occurred.
127         * @throws AccessControlException thrown if the user is not allowed to perform the proxyuser request.
128         */
129        public void validate(String proxyUser, String proxyHost, String doAsUser) throws IOException,
130            AccessControlException {
131            ParamChecker.notEmpty(proxyUser, "proxyUser",
132                    "If you're attempting to use user-impersonation via a proxy user, please make sure that "
133                    + "oozie.service.ProxyUserService.proxyuser.#USER#.hosts and "
134                    + "oozie.service.ProxyUserService.proxyuser.#USER#.groups are configured correctly");
135            ParamChecker.notEmpty(proxyHost, "proxyHost",
136                    "If you're attempting to use user-impersonation via a proxy user, please make sure that "
137                    + "oozie.service.ProxyUserService.proxyuser." + proxyUser + ".hosts and "
138                    + "oozie.service.ProxyUserService.proxyuser." + proxyUser + ".groups are configured correctly");
139            ParamChecker.notEmpty(doAsUser, "doAsUser");
140            LOG.debug("Authorization check proxyuser [{0}] host [{1}] doAs [{2}]",
141                      new Object[]{proxyUser, proxyHost, doAsUser});
142            if (proxyUserHosts.containsKey(proxyUser)) {
143                proxyHost = normalizeHostname(proxyHost);
144                validateRequestorHost(proxyUser, proxyHost, proxyUserHosts.get(proxyUser));
145                validateGroup(proxyUser, doAsUser, proxyUserGroups.get(proxyUser));
146            }
147            else {
148                throw new AccessControlException(MessageFormat.format("User [{0}] not defined as proxyuser", proxyUser));
149            }
150        }
151    
152        private void validateRequestorHost(String proxyUser, String hostname, Set<String> validHosts)
153            throws IOException, AccessControlException {
154            if (validHosts != null) {
155                if (!validHosts.contains(hostname) && !validHosts.contains(normalizeHostname(hostname))) {
156                    throw new AccessControlException(MessageFormat.format("Unauthorized host [{0}] for proxyuser [{1}]",
157                                                                          hostname, proxyUser));
158                }
159            }
160        }
161    
162        private void validateGroup(String proxyUser, String user, Set<String> validGroups) throws IOException,
163            AccessControlException {
164            if (validGroups != null) {
165                List<String> userGroups = services.get(GroupsService.class).getGroups(user);
166                for (String g : validGroups) {
167                    if (userGroups.contains(g)) {
168                        return;
169                    }
170                }
171                throw new AccessControlException(
172                    MessageFormat.format("Unauthorized proxyuser [{0}] for user [{1}], not in proxyuser groups",
173                                         proxyUser, user));
174            }
175        }
176    
177        private String normalizeHostname(String name) {
178            try {
179                InetAddress address = InetAddress.getByName(name);
180                return address.getCanonicalHostName();
181            }
182            catch (IOException ex) {
183                throw new AccessControlException(MessageFormat.format("Could not resolve host [{0}], {1}", name,
184                                                                      ex.getMessage()));
185            }
186        }
187    
188        /**
189         * Destroys the service.
190         */
191        @Override
192        public void destroy() {
193        }
194    
195    }