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 */
018package org.apache.oozie.service;
019
020import org.apache.oozie.ErrorCode;
021import org.apache.oozie.util.ParamChecker;
022import org.apache.oozie.util.XLog;
023
024import java.io.IOException;
025import java.net.InetAddress;
026import java.security.AccessControlException;
027import java.text.MessageFormat;
028import java.util.Arrays;
029import java.util.HashMap;
030import java.util.HashSet;
031import java.util.List;
032import java.util.Map;
033import 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 */
047public 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}