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