LENS-1509 : Lens Server SPNEGO authentication
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
+
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();
+ }
+ }
+}
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();
+ }
+}
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 {
+}
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;
+ }
+ }
+}
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);
+ }
+
+}
+
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();
+ }
+}