LENS-1509 : Lens Server SPNEGO authentication
Project: http://git-wip-us.apache.org/repos/asf/lens/repo Commit: http://git-wip-us.apache.org/repos/asf/lens/commit/3b10f16c Tree: http://git-wip-us.apache.org/repos/asf/lens/tree/3b10f16c Diff: http://git-wip-us.apache.org/repos/asf/lens/diff/3b10f16c Branch: refs/heads/master Commit: 3b10f16cd83e02c6ec67b9d83f2ac513091dddbf Parents: e40f2de Author: Rajitha R <[email protected]> Authored: Fri May 18 19:11:20 2018 +0530 Committer: Rajitha.R <[email protected]> Committed: Fri May 18 19:11:20 2018 +0530 ---------------------------------------------------------------------- contrib/clients/python/lens/client/auth.py | 83 +++++ .../org/apache/lens/api/auth/AuthScheme.java | 49 +++ .../apache/lens/client/SpnegoClientFilter.java | 316 +++++++++++++++++++ .../apache/lens/server/auth/Authenticate.java | 33 ++ .../lens/server/auth/LensSecurityContext.java | 69 ++++ .../server/auth/SpnegoAuthenticationFilter.java | 277 ++++++++++++++++ .../error/NotAuthorizedExceptionMapper.java | 32 ++ 7 files changed, 859 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/lens/blob/3b10f16c/contrib/clients/python/lens/client/auth.py ---------------------------------------------------------------------- diff --git a/contrib/clients/python/lens/client/auth.py b/contrib/clients/python/lens/client/auth.py new file mode 100644 index 0000000..fccc75c --- /dev/null +++ b/contrib/clients/python/lens/client/auth.py @@ -0,0 +1,83 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import kerberos +from requests.auth import AuthBase +import subprocess +import threading +from urlparse import urlparse + + +class SpnegoAuth(AuthBase): + def __init__(self, keytab=None, user=None): + self._thread_local = threading.local() + self.keytab = keytab + self.user = user + + def __call__(self, request): + self.init_per_thread_state() + request.register_hook('response', self.handle_response) + self._thread_local.num_401_calls = 1 + return request + + def has_tgt(self): + # if tgt is available return + return subprocess.call(['klist', '-s']) == 0 + + def acquire_tgt(self): + # try to kinit + exit_code = subprocess.call(['kinit', '-k', '-t', self.keytab, self.user]) + if exit_code != 0: + raise Exception("Couldn't acquire TGT") + + def init_per_thread_state(self): + # Ensure state is initialized just once per-thread + if not hasattr(self._thread_local, 'init'): + self._thread_local.init = True + self._thread_local.num_401_calls = None + + def handle_response(self, response, **kwargs): + if response.status_code == 401 and self._thread_local.num_401_calls < 2: + self._thread_local.num_401_calls += 1 + return self.handle_401(response, **kwargs) + + self._thread_local.num_401_calls += 1 + return response + + def handle_401(self, response, **kwargs): + s_auth = response.headers.get('www-authenticate', '') + if "negotiate" in s_auth.lower(): + # try to acquire tgt + if not self.has_tgt() and self.keytab is not None and self.user is not None: + self.acquire_tgt() + host = urlparse(response.url).hostname + spn = 'HTTP/' + host + code, krb_context = kerberos.authGSSClientInit(spn) + kerberos.authGSSClientStep(krb_context, "") + negotiate_details = kerberos.authGSSClientResponse(krb_context) + auth_header = "Negotiate " + negotiate_details + + # Consume content and release the original connection + # to allow our new request to reuse the same one. + response.content + response.close() + + response.request.headers['Authorization'] = auth_header + _resp = response.connection.send(response.request, **kwargs) + return _resp + + return response + http://git-wip-us.apache.org/repos/asf/lens/blob/3b10f16c/lens-api/src/main/java/org/apache/lens/api/auth/AuthScheme.java ---------------------------------------------------------------------- diff --git a/lens-api/src/main/java/org/apache/lens/api/auth/AuthScheme.java b/lens-api/src/main/java/org/apache/lens/api/auth/AuthScheme.java new file mode 100644 index 0000000..d8434fd --- /dev/null +++ b/lens-api/src/main/java/org/apache/lens/api/auth/AuthScheme.java @@ -0,0 +1,49 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.lens.api.auth; + +import java.util.Optional; + +import org.apache.commons.lang.StringUtils; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum AuthScheme { + BASIC("Basic"), + DIGEST("Digest"), + NEGOTIATE("Negotiate"), + ; + + @Getter + private final String name; + + public static Optional<AuthScheme> getFromString(String value) { + if (StringUtils.isBlank(value)) { + return Optional.empty(); + } + try { + return Optional.of(AuthScheme.valueOf(value)); + } catch (IllegalArgumentException e) { + return Optional.empty(); + } + } +} http://git-wip-us.apache.org/repos/asf/lens/blob/3b10f16c/lens-client/src/main/java/org/apache/lens/client/SpnegoClientFilter.java ---------------------------------------------------------------------- diff --git a/lens-client/src/main/java/org/apache/lens/client/SpnegoClientFilter.java b/lens-client/src/main/java/org/apache/lens/client/SpnegoClientFilter.java new file mode 100644 index 0000000..87696aa --- /dev/null +++ b/lens-client/src/main/java/org/apache/lens/client/SpnegoClientFilter.java @@ -0,0 +1,316 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.lens.client; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Base64; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.security.auth.Subject; +import javax.security.auth.kerberos.KerberosPrincipal; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.ClientResponseFilter; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import org.apache.commons.lang.StringUtils; + +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * A client filter for Jersey client which supports SPNEGO authentication. + * + * Currently only "Negotiate" scheme is supported which will do auth using Kerberos. + * + * A user can use his/her keytab and userprincipal in lens-client-site.xml + * using config "lens.client.authentication.kerberos.keytab" and "lens.client.authentication.kerberos.principal" + * respectively. If these config is not provided then kerberos credential cache is used by default. + */ +@Slf4j +@RequiredArgsConstructor +public class SpnegoClientFilter implements ClientRequestFilter, ClientResponseFilter{ + private static final String REQUEST_PROPERTY_FILTER_REUSED = + "org.glassfish.jersey.client.authentication.HttpAuthenticationFilter.reused"; + private static final String SPNEGO_OID = "1.3.6.1.5.5.2"; + private static final String NEGOTIATE_SCHEME = "Negotiate"; + + private static final LensClientConfig CONF = new LensClientConfig(); + private final String keyTabLocation = CONF.get(LensClientConfig.KERBEROS_KEYTAB); + private final String userPrincipal = CONF.get(LensClientConfig.KERBEROS_PRINCIPAL); + private final String realm = CONF.get(LensClientConfig.KERBEROS_REALM); + + private String servicePrincipalName; + private boolean useCanonicalHostname; + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + + } + + @Override + public void filter(ClientRequestContext request, ClientResponseContext response) throws IOException { + if ("true".equals(request.getProperty(REQUEST_PROPERTY_FILTER_REUSED))) { + return; + } + boolean authenticate; + + if (response.getStatus() == Response.Status.UNAUTHORIZED.getStatusCode()) { + String authString = response.getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE); + if (authString != null) { + if (authString.trim().startsWith(NEGOTIATE_SCHEME)) { + authenticate = true; + } else { + return; + } + } else { + authenticate = false; + } + + if (authenticate) { + String authorization = getAuthorization(request.getUri()); + repeatRequest(request, response, authorization); + } + } + } + + + + private String getAuthorization(URI currentURI) { + try { + String spn = getCompleteServicePrincipalName(currentURI); + + Oid oid = new Oid(SPNEGO_OID); + + byte[] token = getToken(spn, oid); + String encodedToken = new String(Base64.getEncoder().encode(token), StandardCharsets.UTF_8); + return NEGOTIATE_SCHEME + " " + encodedToken; + } catch (LoginException e) { + throw new RuntimeException(e.getMessage(), e); + } catch (GSSException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + + private byte[] getToken(String spn, Oid oid) throws GSSException, LoginException { + LoginContext lc = buildLoginContext(); + lc.login(); + Subject subject = lc.getSubject(); + + GSSManager manager = GSSManager.getInstance(); + GSSName serverName = manager.createName(spn, null); // 2nd oid + + GSSContext context = manager + .createContext(serverName.canonicalize(oid), oid, null, GSSContext.DEFAULT_LIFETIME); + + final byte[] token = new byte[0]; + + try { + return Subject.doAs(subject, new CreateServiceTicketAction(context, token)); + } catch (PrivilegedActionException e) { + if (e.getCause() instanceof GSSException) { + throw (GSSException) e.getCause(); + } + log.error("initSecContext", e); + return null; + } + } + + + private String getCompleteServicePrincipalName(URI currentURI) { + String name; + + if (servicePrincipalName == null) { + String host = currentURI.getHost(); + if (useCanonicalHostname) { + host = getCanonicalHostname(host); + } + name = "HTTP/" + host; + } else { + name = servicePrincipalName; + } + if (realm != null) { + name += "@" + realm; + } + + return name; + } + + private String getCanonicalHostname(String hostname) { + String canonicalHostname = hostname; + try { + InetAddress in = InetAddress.getByName(hostname); + canonicalHostname = in.getCanonicalHostName(); + log.debug("resolved hostname=" + hostname + " to canonicalHostname=" + canonicalHostname); + } catch (Exception e) { + log.warn("unable to resolve canonical hostname", e); + } + return canonicalHostname; + } + + + private static final class CreateServiceTicketAction implements PrivilegedExceptionAction<byte[]> { + private final GSSContext context; + private final byte[] token; + + private CreateServiceTicketAction(GSSContext context, byte[] token) { + this.context = context; + this.token = token; + } + + public byte[] run() throws GSSException { + byte[] data = context.initSecContext(token, 0, token.length); + return data; + } + } + + @RequiredArgsConstructor + static class ClientLoginConfig extends Configuration { + private final String keyTabLocation; + private final String userPrincipal; + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + Map<String, Object> options = new HashMap<String, Object>(); + + // if we don't have keytab or principal only option is to rely on + // credentials cache. + if (StringUtils.isEmpty(keyTabLocation) || StringUtils.isEmpty(userPrincipal)) { + // cache + options.put("useTicketCache", "true"); + } else { + // keytab + options.put("useKeyTab", "true"); + options.put("keyTab", keyTabLocation); + options.put("principal", userPrincipal); + options.put("storeKey", "true"); + } + + options.put("doNotPrompt", "true"); + options.put("isInitiator", "true"); + + return new AppConfigurationEntry[] { new AppConfigurationEntry( + "com.sun.security.auth.module.Krb5LoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options), }; + } + + } + + private LoginContext buildLoginContext() throws LoginException { + ClientLoginConfig loginConfig = new ClientLoginConfig(keyTabLocation, userPrincipal); + + Subject subject = null; + if (StringUtils.isNotBlank(keyTabLocation) && StringUtils.isNotBlank(userPrincipal)) { + Set<Principal> princ = new HashSet<>(1); + princ.add(new KerberosPrincipal(userPrincipal)); + subject = new Subject(false, princ, new HashSet<>(), new HashSet<>()); + } + LoginContext lc = new LoginContext("", subject, null, loginConfig); + return lc; + } + + + /** + * Repeat the {@code request} with provided {@code newAuthorizationHeader} + * and update the {@code response} with newest response data. + * + * @param request Request context. + * @param response Response context (will be updated with the new response data). + * @param newAuthorizationHeader {@code Authorization} header that should be added to the new request. + * @return {@code true} is the authentication was successful ({@code true} if 401 response code was not returned; + * {@code false} otherwise). + */ + private boolean repeatRequest(ClientRequestContext request, + ClientResponseContext response, + String newAuthorizationHeader) { + Client client = ClientBuilder.newClient(request.getConfiguration()); + String method = request.getMethod(); + MediaType mediaType = request.getMediaType(); + URI lUri = request.getUri(); + + WebTarget resourceTarget = client.target(lUri); + + Invocation.Builder builder = resourceTarget.request(mediaType); + + MultivaluedMap<String, Object> newHeaders = new MultivaluedHashMap<String, Object>(); + + for (Map.Entry<String, List<Object>> entry : request.getHeaders().entrySet()) { + if (HttpHeaders.AUTHORIZATION.equals(entry.getKey())) { + continue; + } + newHeaders.put(entry.getKey(), entry.getValue()); + } + + newHeaders.add(HttpHeaders.AUTHORIZATION, newAuthorizationHeader); + builder.headers(newHeaders); + + builder.property(REQUEST_PROPERTY_FILTER_REUSED, "true"); + + Invocation invocation; + if (request.getEntity() == null) { + invocation = builder.build(method); + } else { + invocation = builder.build(method, + Entity.entity(request.getEntity(), request.getMediaType())); + } + Response nextResponse = invocation.invoke(); + + if (nextResponse.hasEntity()) { + response.setEntityStream(nextResponse.readEntity(InputStream.class)); + } + MultivaluedMap<String, String> headers = response.getHeaders(); + headers.clear(); + headers.putAll(nextResponse.getStringHeaders()); + response.setStatus(nextResponse.getStatus()); + + return response.getStatus() != Response.Status.UNAUTHORIZED.getStatusCode(); + } +} http://git-wip-us.apache.org/repos/asf/lens/blob/3b10f16c/lens-server/src/main/java/org/apache/lens/server/auth/Authenticate.java ---------------------------------------------------------------------- diff --git a/lens-server/src/main/java/org/apache/lens/server/auth/Authenticate.java b/lens-server/src/main/java/org/apache/lens/server/auth/Authenticate.java new file mode 100644 index 0000000..44b0040 --- /dev/null +++ b/lens-server/src/main/java/org/apache/lens/server/auth/Authenticate.java @@ -0,0 +1,33 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.lens.server.auth; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the JAX-RS resource class/method will be authenticated by a filter. + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Authenticate { +} http://git-wip-us.apache.org/repos/asf/lens/blob/3b10f16c/lens-server/src/main/java/org/apache/lens/server/auth/LensSecurityContext.java ---------------------------------------------------------------------- diff --git a/lens-server/src/main/java/org/apache/lens/server/auth/LensSecurityContext.java b/lens-server/src/main/java/org/apache/lens/server/auth/LensSecurityContext.java new file mode 100644 index 0000000..c7f73a8 --- /dev/null +++ b/lens-server/src/main/java/org/apache/lens/server/auth/LensSecurityContext.java @@ -0,0 +1,69 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.lens.server.auth; + +import java.security.Principal; + +import javax.ws.rs.core.SecurityContext; + +import lombok.RequiredArgsConstructor; + +/** + * Implementation of {@link SecurityContext} which you can inject in a resource class + * authenticated by lens auth filter. + */ +public class LensSecurityContext implements SecurityContext { + private final Principal principal; + private final String authScheme; + + public LensSecurityContext(String username, String authScheme) { + principal = new SimplePrincipal(username); + this.authScheme = authScheme; + } + + @Override + public Principal getUserPrincipal() { + return principal; + } + + @Override + public boolean isUserInRole(String role) { + return false; + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public String getAuthenticationScheme() { + return authScheme; + } + + @RequiredArgsConstructor + private static class SimplePrincipal implements Principal { + private final String name; + + @Override + public String getName() { + return name; + } + } +} http://git-wip-us.apache.org/repos/asf/lens/blob/3b10f16c/lens-server/src/main/java/org/apache/lens/server/auth/SpnegoAuthenticationFilter.java ---------------------------------------------------------------------- diff --git a/lens-server/src/main/java/org/apache/lens/server/auth/SpnegoAuthenticationFilter.java b/lens-server/src/main/java/org/apache/lens/server/auth/SpnegoAuthenticationFilter.java new file mode 100644 index 0000000..a6a0abf --- /dev/null +++ b/lens-server/src/main/java/org/apache/lens/server/auth/SpnegoAuthenticationFilter.java @@ -0,0 +1,277 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.lens.server.auth; + +import java.io.File; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.annotation.Priority; +import javax.security.auth.Subject; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.Priorities; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.UriInfo; + +import org.apache.lens.api.auth.AuthScheme; +import org.apache.lens.server.LensServerConf; +import org.apache.lens.server.api.LensConfConstants; + +import org.apache.commons.lang3.StringUtils; + +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; + +import lombok.extern.slf4j.Slf4j; + +/** + * A JAX-RS filter for SPNEGO authentication. + * + * <p>Currently only "Negotiate" scheme is supported which will do auth using Kerberos.</p> + * + * <p>This filter can be enabled by adding an entry in {@code lens.server.ws.filternames} property and providing + * the impl class.</p> + * + * <pre>The following configuration is needed for the filter to function + * {@code lens.server.authentication.scheme} : NEGOTIATE (other values which are not supported are listed + * in {@link AuthScheme}) + * {@code lens.server.authentication.kerberos.principal} : The SPN (in format HTTP/fqdn) + * {@code lens.server.authentication.kerberos.keytab} : Keytab of lens SPN + * </pre> + */ + +@Slf4j +@Priority(Priorities.AUTHENTICATION) +public class SpnegoAuthenticationFilter implements ContainerRequestFilter { + private static final String SPNEGO_OID = "1.3.6.1.5.5.2"; + private static final String KERBEROS_LOGIN_MODULE_NAME = + "com.sun.security.auth.module.Krb5LoginModule"; + + private static final org.apache.hadoop.conf.Configuration CONF = LensServerConf.getHiveConf(); + private static final AuthScheme AUTH_SCHEME = AuthScheme.valueOf(CONF.get(LensConfConstants.AUTH_SCHEME)); + + static { + if (AUTH_SCHEME != AuthScheme.NEGOTIATE) { + log.error("Lens server currently only supports NEGOTIATE auth scheme"); + throw new RuntimeException("Lens server currently only supports NEGOTIATE auth scheme"); + } + } + + private String servicePrincipalName = CONF.get(LensConfConstants.KERBEROS_PRINCIPAL); + private String realm = CONF.get(LensConfConstants.KERBEROS_REALM); + private Configuration loginConfig = getJaasKrb5TicketConfig(servicePrincipalName, + new File(CONF.get(LensConfConstants.KERBEROS_KEYTAB))); + + private HttpHeaders headers; + + private UriInfo uriInfo; + + private ResourceInfo resourceInfo; + + @Context + public void setHeaders(HttpHeaders headers) { + this.headers = headers; + } + + @Context + public void setUriInfo(UriInfo uriInfo) { + this.uriInfo = uriInfo; + } + + @Context + public void setResourceInfo(ResourceInfo resourceInfo) { + this.resourceInfo = resourceInfo; + } + + @Override + public void filter(ContainerRequestContext context) { + // only authenticate when @Authenticate is present on resource + if (resourceInfo.getResourceClass() == null || resourceInfo.getResourceMethod() == null) { + return; + } + if (!(resourceInfo.getResourceClass().isAnnotationPresent(Authenticate.class) + || resourceInfo.getResourceMethod().isAnnotationPresent(Authenticate.class))) { + return; + } + List<String> authHeaders = headers + .getRequestHeader(HttpHeaders.AUTHORIZATION); + if (authHeaders == null || authHeaders.size() != 1) { + log.info("No Authorization header is available"); + throw toNotAuthorizedException(null, getFaultResponse()); + } + String[] authPair = StringUtils.split(authHeaders.get(0), " "); + if (authPair.length != 2 || !AuthScheme.NEGOTIATE.getName().equalsIgnoreCase(authPair[0])) { + log.info("Negotiate Authorization scheme is expected"); + throw toNotAuthorizedException(null, getFaultResponse()); + } + + byte[] serviceTicket = getServiceTicket(authPair[1]); + + try { + Subject serviceSubject = loginAndGetSubject(); + + GSSContext gssContext = createGSSContext(); + + Subject.doAs(serviceSubject, new ValidateServiceTicketAction(gssContext, serviceTicket)); + + final GSSName srcName = gssContext.getSrcName(); + if (srcName == null) { + throw toNotAuthorizedException(null, getFaultResponse()); + } + + String complexUserName = srcName.toString(); + + String simpleUserName = complexUserName; + int index = simpleUserName.lastIndexOf('@'); + if (index > 0) { + simpleUserName = simpleUserName.substring(0, index); + } + context.setSecurityContext(createSecurityContext(simpleUserName, AUTH_SCHEME.getName())); + if (!gssContext.getCredDelegState()) { + gssContext.dispose(); + gssContext = null; + } + + } catch (LoginException e) { + log.info("Unsuccessful JAAS login for the service principal: " + e.getMessage()); + throw toNotAuthorizedException(e, getFaultResponse()); + } catch (GSSException e) { + log.info("GSS API exception: " + e.getMessage()); + throw toNotAuthorizedException(e, getFaultResponse()); + } catch (PrivilegedActionException e) { + log.info("PrivilegedActionException: " + e.getMessage()); + throw toNotAuthorizedException(e, getFaultResponse()); + } + } + + private SecurityContext createSecurityContext(String simpleUserName, String authScheme) { + return new LensSecurityContext(simpleUserName, authScheme); + } + + private GSSContext createGSSContext() throws GSSException { + Oid oid = new Oid(SPNEGO_OID); + GSSManager gssManager = GSSManager.getInstance(); + + String spn = getCompleteServicePrincipalName(); + GSSName gssService = gssManager.createName(spn, null); + + return gssManager.createContext(gssService.canonicalize(oid), + oid, null, GSSContext.DEFAULT_LIFETIME); + } + + private Subject loginAndGetSubject() throws LoginException { + + // The login without a callback can work if + // - Kerberos keytabs are used with a principal name set in the JAAS config + // - Kerberos is integrated into the OS logon process + // meaning that a process which runs this code has the + // user identity + + LoginContext lc = null; + if (loginConfig != null) { + lc = new LoginContext("", null, null, loginConfig); + } else { + log.info("LoginContext can not be initialized"); + throw new LoginException(); + } + lc.login(); + return lc.getSubject(); + } + + private byte[] getServiceTicket(String encodedServiceTicket) { + try { + return java.util.Base64.getDecoder().decode(encodedServiceTicket); + } catch (IllegalArgumentException ex) { + throw toNotAuthorizedException(null, getFaultResponse()); + } + } + + private static Response getFaultResponse() { + return Response.status(401).header(HttpHeaders.WWW_AUTHENTICATE, AuthScheme.NEGOTIATE.getName()).build(); + } + + private String getCompleteServicePrincipalName() { + String name = servicePrincipalName == null + ? "HTTP/" + uriInfo.getBaseUri().getHost() : servicePrincipalName; + if (realm != null) { + name += "@" + realm; + } + return name; + } + + private static final class ValidateServiceTicketAction implements PrivilegedExceptionAction<byte[]> { + private final GSSContext context; + private final byte[] token; + + private ValidateServiceTicketAction(GSSContext context, byte[] token) { + this.context = context; + this.token = token; + } + + public byte[] run() throws GSSException { + byte[] data = context.acceptSecContext(token, 0, token.length); + return data; + } + } + + private static Configuration getJaasKrb5TicketConfig( + final String principal, final File keytab) { + return new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + Map<String, String> options = new HashMap<>(); + options.put("principal", principal); + options.put("keyTab", keytab.getAbsolutePath()); + options.put("doNotPrompt", "true"); + options.put("useKeyTab", "true"); + options.put("storeKey", "true"); + options.put("isInitiator", "false"); + + return new AppConfigurationEntry[] { + new AppConfigurationEntry(KERBEROS_LOGIN_MODULE_NAME, + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options), + }; + } + }; + } + + private WebApplicationException toNotAuthorizedException(Throwable cause, Response resp) { + return new NotAuthorizedException(resp, cause); + } + +} + http://git-wip-us.apache.org/repos/asf/lens/blob/3b10f16c/lens-server/src/main/java/org/apache/lens/server/error/NotAuthorizedExceptionMapper.java ---------------------------------------------------------------------- diff --git a/lens-server/src/main/java/org/apache/lens/server/error/NotAuthorizedExceptionMapper.java b/lens-server/src/main/java/org/apache/lens/server/error/NotAuthorizedExceptionMapper.java new file mode 100644 index 0000000..4cf0185 --- /dev/null +++ b/lens-server/src/main/java/org/apache/lens/server/error/NotAuthorizedExceptionMapper.java @@ -0,0 +1,32 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.lens.server.error; + +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +public class NotAuthorizedExceptionMapper implements ExceptionMapper<NotAuthorizedException> { + @Override + public Response toResponse(NotAuthorizedException exception) { + return exception.getResponse(); + } +}
