You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ranger.apache.org by sp...@apache.org on 2021/08/27 14:14:43 UTC

[ranger] branch ranger-2.2 updated: RANGER-3363: Added support in ranger admin for handling session timeout requests with knox proxy

This is an automated email from the ASF dual-hosted git repository.

spolavarapu pushed a commit to branch ranger-2.2
in repository https://gitbox.apache.org/repos/asf/ranger.git


The following commit(s) were added to refs/heads/ranger-2.2 by this push:
     new fd90d0d  RANGER-3363: Added support in ranger admin for handling session timeout requests with knox proxy
fd90d0d is described below

commit fd90d0d4d37b2a2ca4c82b641929153a4dfb31dd
Author: Sailaja Polavarapu <sp...@cloudera.com>
AuthorDate: Fri Aug 27 07:12:49 2021 -0700

    RANGER-3363: Added support in ranger admin for handling session timeout requests with knox proxy
---
 .../main/java/org/apache/ranger/rest/UserREST.java |  7 ++
 .../RangerAuthenticationEntryPoint.java            |  4 +-
 .../web/filter/RangerKRBAuthenticationFilter.java  | 44 +++++++++-
 .../web/filter/RangerSSOAuthenticationFilter.java  | 96 ++--------------------
 .../main/java/org/apache/ranger/util/RestUtil.java | 85 +++++++++++++++++++
 .../java/org/apache/ranger/view/VXPortalUser.java  | 15 ++++
 6 files changed, 158 insertions(+), 93 deletions(-)

diff --git a/security-admin/src/main/java/org/apache/ranger/rest/UserREST.java b/security-admin/src/main/java/org/apache/ranger/rest/UserREST.java
index ffdf101..f91d7f4 100644
--- a/security-admin/src/main/java/org/apache/ranger/rest/UserREST.java
+++ b/security-admin/src/main/java/org/apache/ranger/rest/UserREST.java
@@ -20,7 +20,9 @@
  package org.apache.ranger.rest;
 
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.Consumes;
@@ -37,6 +39,7 @@ import org.apache.log4j.Logger;
 import org.apache.ranger.biz.UserMgr;
 import org.apache.ranger.biz.XUserMgr;
 import org.apache.ranger.common.MessageEnums;
+import org.apache.ranger.common.PropertiesUtil;
 import org.apache.ranger.common.RESTErrorUtil;
 import org.apache.ranger.common.RangerConfigUtil;
 import org.apache.ranger.common.RangerConstants;
@@ -277,7 +280,11 @@ public class UserREST {
 		try {
 			logger.debug("getUserProfile(). httpSessionId="
 					+ request.getSession().getId());
+			Map<String, String> configProperties  = new HashMap<>();
+			Long inactivityTimeout = PropertiesUtil.getLongProperty("ranger.service.inactivity.timeout", 15*60);
+			configProperties.put("inactivityTimeout", Long.toString(inactivityTimeout));
 			VXPortalUser userProfile = userManager.getUserProfileByLoginId();
+			userProfile.setConfigProperties(configProperties);
 			return userProfile;
 		} catch (Throwable t) {
 			logger.error(
diff --git a/security-admin/src/main/java/org/apache/ranger/security/web/authentication/RangerAuthenticationEntryPoint.java b/security-admin/src/main/java/org/apache/ranger/security/web/authentication/RangerAuthenticationEntryPoint.java
index 3e56d9f..c2e4a3e 100644
--- a/security-admin/src/main/java/org/apache/ranger/security/web/authentication/RangerAuthenticationEntryPoint.java
+++ b/security-admin/src/main/java/org/apache/ranger/security/web/authentication/RangerAuthenticationEntryPoint.java
@@ -33,7 +33,7 @@ import org.apache.ranger.biz.SessionMgr;
 import org.apache.ranger.common.JSONUtil;
 import org.apache.ranger.common.PropertiesUtil;
 import org.apache.ranger.common.RangerConfigUtil;
-import org.apache.ranger.security.web.filter.RangerSSOAuthenticationFilter;
+import org.apache.ranger.util.RestUtil;
 import org.apache.ranger.view.VXResponse;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.core.AuthenticationException;
@@ -126,7 +126,7 @@ public class RangerAuthenticationEntryPoint extends
 			}
 			response.sendError(ajaxReturnCode, "");
 		} else if (!(requestURL.contains(servletPath))) {
-			if(requestURL.contains(RangerSSOAuthenticationFilter.LOCAL_LOGIN_URL)){
+			if(requestURL.contains(RestUtil.LOCAL_LOGIN_URL)){
 				if (request.getSession() != null){
 					request.getSession().setAttribute("locallogin","true");
 					request.getServletContext().setAttribute(request.getSession().getId(), "locallogin");
diff --git a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerKRBAuthenticationFilter.java b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerKRBAuthenticationFilter.java
index 9877e14..c0ff06e 100644
--- a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerKRBAuthenticationFilter.java
+++ b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerKRBAuthenticationFilter.java
@@ -52,6 +52,7 @@ import javax.servlet.descriptor.JspConfigDescriptor;
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
 
 import org.apache.commons.collections.iterators.IteratorEnumeration;
 import org.apache.hadoop.conf.Configuration;
@@ -65,6 +66,7 @@ import org.apache.ranger.biz.UserMgr;
 import org.apache.ranger.common.PropertiesUtil;
 import org.apache.ranger.common.RESTErrorUtil;
 import org.apache.ranger.security.handler.RangerAuthenticationProvider;
+import org.apache.ranger.util.RestUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -113,6 +115,8 @@ public class RangerKRBAuthenticationFilter extends RangerKrbFilter {
 
 	private static final String KERBEROS_TYPE = "kerberos";
 	private static final String S_USER = "suser";
+	private String originalUrlQueryParam = "originalUrl";
+	public static final String LOGOUT_URL = "/logout";
 
 	public RangerKRBAuthenticationFilter() {
 		try {
@@ -395,11 +399,45 @@ public class RangerKRBAuthenticationFilter extends RangerKrbFilter {
 					throw restErrorUtil.createRESTException("RangerKRBAuthenticationFilter Failed : "+e.getMessage());
 				}				
 			}	
-		}else{
-			filterChain.doFilter(request, response);
+		} else {
+			String action = httpRequest.getParameter("action");
+			String doAsUser = request.getParameter("doAs");
+			if(LOG.isDebugEnabled()) {
+				LOG.debug("RangerKRBAuthenticationFilter: request URL = " + httpRequest.getRequestURI());
+			}
+
+			boolean allowTrustedProxy = PropertiesUtil.getBooleanProperty(ALLOW_TRUSTED_PROXY, false);
+
+			if (allowTrustedProxy && StringUtils.isNotEmpty(doAsUser) && existingAuth.isAuthenticated()
+					&& StringUtils.equals(action, RestUtil.TIMEOUT_ACTION)) {
+				HttpServletResponse httpResponse = (HttpServletResponse) response;
+				String xForwardedURL = RestUtil.constructForwardableURL(httpRequest);
+				if (LOG.isDebugEnabled()) {
+					LOG.debug("xForwardedURL = " + xForwardedURL);
+				}
+				String logoutUrl = xForwardedURL;
+				logoutUrl =  StringUtils.replace(logoutUrl, httpRequest.getRequestURI(), LOGOUT_URL);
+				if (LOG.isDebugEnabled()) {
+					LOG.debug("logoutUrl value is " + logoutUrl);
+				}
+				String redirectUrl = RestUtil.constructRedirectURL(httpRequest, logoutUrl, xForwardedURL, originalUrlQueryParam);
+
+				if (LOG.isDebugEnabled()) {
+					LOG.debug("Redirect URL = " + redirectUrl);
+					LOG.debug("session id = " + httpRequest.getRequestedSessionId());
+				}
+
+				HttpSession httpSession = httpRequest.getSession(false);
+				if (httpSession != null) {
+					httpSession.invalidate();
+				}
+				httpResponse.sendRedirect(redirectUrl);
+			} else {
+				filterChain.doFilter(request, response);
+			}
 		}
 	}
-	
+
 	private boolean isSpnegoEnable(String authType){
 		String principal = PropertiesUtil.getProperty(PRINCIPAL);
 		String keytabPath = PropertiesUtil.getProperty(KEYTAB);
diff --git a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSSOAuthenticationFilter.java b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSSOAuthenticationFilter.java
index 6d35991..abbf2d9 100644
--- a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSSOAuthenticationFilter.java
+++ b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSSOAuthenticationFilter.java
@@ -26,6 +26,7 @@ import com.nimbusds.jose.JWSVerifier;
 import com.nimbusds.jose.crypto.RSASSAVerifier;
 import com.nimbusds.jwt.SignedJWT;
 
+import org.apache.ranger.util.RestUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -53,7 +54,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Date;
-import java.util.Enumeration;
 import java.util.List;
 
 import org.apache.commons.lang.StringUtils;
@@ -88,9 +88,7 @@ public class RangerSSOAuthenticationFilter implements Filter {
     public static final String JWT_EXPECTED_SIGALG = "ranger.sso.expected.sigalg";
     public static final String JWT_DEFAULT_SIGALG = "RS256";
 
-	public static final String LOCAL_LOGIN_URL = "locallogin";
 	public static final String DEFAULT_BROWSER_USERAGENT = "ranger.default.browser-useragents";
-    public static final String PROXY_RANGER_URL_PATH = "/ranger";
 
 
 	private SSOAuthenticationProperties jwtProperties;
@@ -130,7 +128,7 @@ public class RangerSSOAuthenticationFilter implements Filter {
 
 		HttpServletRequest httpRequest = (HttpServletRequest)servletRequest;
 
-                String xForwardedURL = constructForwardableURL(httpRequest);
+                String xForwardedURL = RestUtil.constructForwardableURL(httpRequest);
 
 		if (httpRequest.getRequestedSessionId() != null && !httpRequest.isRequestedSessionIdValid()){
 			synchronized(httpRequest.getServletContext()){
@@ -154,7 +152,7 @@ public class RangerSSOAuthenticationFilter implements Filter {
 			}
 		}
 		//If sso is enable and request is not for local login and is from browser then it will go inside and try for knox sso authentication
-		if (ssoEnabled && !httpRequest.getRequestURI().contains(LOCAL_LOGIN_URL)) {
+		if (ssoEnabled && !httpRequest.getRequestURI().contains(RestUtil.LOCAL_LOGIN_URL)) {
 			//if jwt properties are loaded and is current not authenticated then it will go for sso authentication
 			//Note : Need to remove !isAuthenticated() after knoxsso solve the bug from cross-origin script
 			if (jwtProperties != null && !isAuthenticated()) {
@@ -203,7 +201,7 @@ public class RangerSSOAuthenticationFilter implements Filter {
 									httpServletResponse.setStatus(RangerConstants.SC_AUTHENTICATION_TIMEOUT);
 									httpServletResponse.setHeader("X-Rngr-Redirect-Url", ssourl);
 								} else {
-									ssourl = constructLoginURL(httpRequest, xForwardedURL);
+									ssourl = RestUtil.constructRedirectURL(httpRequest, authenticationProviderUrl, xForwardedURL, originalUrlQueryParam);
 									if (LOG.isDebugEnabled()) {
 										LOG.debug("SSO URL = " + ssourl);
 									}
@@ -231,7 +229,7 @@ public class RangerSSOAuthenticationFilter implements Filter {
 							httpServletResponse.setStatus(RangerConstants.SC_AUTHENTICATION_TIMEOUT);
 							httpServletResponse.setHeader("X-Rngr-Redirect-Url", ssourl);
 						} else {
-							ssourl = constructLoginURL(httpRequest, xForwardedURL);
+							ssourl = RestUtil.constructRedirectURL(httpRequest, authenticationProviderUrl, xForwardedURL, originalUrlQueryParam);
 							if (LOG.isDebugEnabled()) {
 								LOG.debug("SSO URL = " + ssourl);
 							}
@@ -246,11 +244,11 @@ public class RangerSSOAuthenticationFilter implements Filter {
 			else {
 				filterChain.doFilter(servletRequest, servletResponse);
 			}
-		} else if(ssoEnabled && ((HttpServletRequest) servletRequest).getRequestURI().contains(LOCAL_LOGIN_URL) && isWebUserAgent(userAgent) && isAuthenticated()){
+		} else if(ssoEnabled && ((HttpServletRequest) servletRequest).getRequestURI().contains(RestUtil.LOCAL_LOGIN_URL) && isWebUserAgent(userAgent) && isAuthenticated()){
 				//If already there's an active session with sso and user want's to switch to local login(i.e without sso) then it won't be navigated to local login
 				// In this scenario the user as to use separate browser
-				String url = ((HttpServletRequest) servletRequest).getRequestURI().replace(LOCAL_LOGIN_URL+"/", "");
-				url = url.replace(LOCAL_LOGIN_URL, "");
+				String url = ((HttpServletRequest) servletRequest).getRequestURI().replace(RestUtil.LOCAL_LOGIN_URL+"/", "");
+				url = url.replace(RestUtil.LOCAL_LOGIN_URL, "");
 				LOG.warn("There is an active session and if you want local login to ranger, try this on a separate browser");
 				((HttpServletResponse)servletResponse).sendRedirect(url);
 		}
@@ -260,57 +258,6 @@ public class RangerSSOAuthenticationFilter implements Filter {
 		}
 	}
 
-	private String constructForwardableURL(HttpServletRequest httpRequest) {
-		String xForwardedProto = "";
-		String xForwardedHost = "";
-		String xForwardedContext = "";
-		Enumeration<?> names = httpRequest.getHeaderNames();
-		while (names.hasMoreElements()) {
-			String name = (String) names.nextElement();
-			Enumeration<?> values = httpRequest.getHeaders(name);
-			String value = "";
-			if (values != null) {
-				while (values.hasMoreElements()) {
-					value = (String) values.nextElement();
-				}
-			}
-			if (StringUtils.trimToNull(name) != null && StringUtils.trimToNull(value) != null) {
-				if (name.equalsIgnoreCase("x-forwarded-proto")) {
-					xForwardedProto = value;
-				} else if (name.equalsIgnoreCase("x-forwarded-host")) {
-					xForwardedHost = value;
-				} else if (name.equalsIgnoreCase("x-forwarded-context")) {
-					xForwardedContext = value;
-				}
-			}
-		}
-		if (xForwardedHost.contains(",")) {
-			if (LOG.isDebugEnabled()) {
-				LOG.debug("xForwardedHost value is " + xForwardedHost + " it contains multiple hosts, selecting the first host.");
-			}
-			xForwardedHost = xForwardedHost.split(",")[0].trim();
-		}
-		String xForwardedURL = "";
-		if (StringUtils.trimToNull(xForwardedProto) != null) {
-			//if header contains x-forwarded-host and x-forwarded-context
-			if (StringUtils.trimToNull(xForwardedHost) != null && StringUtils.trimToNull(xForwardedContext) != null) {
-				xForwardedURL = xForwardedProto + "://" + xForwardedHost + xForwardedContext + PROXY_RANGER_URL_PATH + httpRequest.getRequestURI();
-			} else if (StringUtils.trimToNull(xForwardedHost) != null) {
-				//if header contains x-forwarded-host and does not contains x-forwarded-context
-				xForwardedURL = xForwardedProto + "://" + xForwardedHost + httpRequest.getRequestURI();
-			} else {
-				//if header does not contains x-forwarded-host and x-forwarded-context
-				//preserve the x-forwarded-proto value coming from the request.
-				String requestURL = httpRequest.getRequestURL().toString();
-				if (StringUtils.trimToNull(requestURL) != null && requestURL.startsWith("http:")) {
-					requestURL = requestURL.replaceFirst("http", xForwardedProto);
-				}
-				xForwardedURL = requestURL;
-			}
-		}
-		return xForwardedURL;
-	}
-
 	private Authentication getGrantedAuthority(Authentication authentication) {
 		UsernamePasswordAuthenticationToken result=null;
 		if(authentication!=null && authentication.isAuthenticated()){
@@ -394,33 +341,6 @@ public class RangerSSOAuthenticationFilter implements Filter {
 	}
 
 	/**
-	 * Create the URL to be used for authentication of the user in the absence
-	 * of a JWT token within the incoming request.
-	 *
-	 * @param request
-	 *            for getting the original request URL
-	 * @return url to use as login url for redirect
-	 */
-        protected String constructLoginURL(HttpServletRequest request, String xForwardedURL) {
-		String delimiter = "?";
-		if (authenticationProviderUrl.contains("?")) {
-			delimiter = "&";
-		}
-                String loginURL = authenticationProviderUrl + delimiter + originalUrlQueryParam + "=";
-                if (StringUtils.trimToNull(xForwardedURL) != null) {
-                        loginURL += xForwardedURL + getOriginalQueryString(request);
-                } else {
-                        loginURL += request.getRequestURL().append(getOriginalQueryString(request));
-                }
-		return loginURL;
-	}
-
-	private String getOriginalQueryString(HttpServletRequest request) {
-		String originalQueryString = request.getQueryString();
-		return (originalQueryString == null) ? "" : "?" + originalQueryString;
-	}
-
-	/**
 	 * This method provides a single method for validating the JWT for use in
 	 * request processing. It provides for the override of specific aspects of
 	 * this implementation through submethods used within but also allows for
diff --git a/security-admin/src/main/java/org/apache/ranger/util/RestUtil.java b/security-admin/src/main/java/org/apache/ranger/util/RestUtil.java
index 4d7388f..b66a7ae 100644
--- a/security-admin/src/main/java/org/apache/ranger/util/RestUtil.java
+++ b/security-admin/src/main/java/org/apache/ranger/util/RestUtil.java
@@ -22,13 +22,22 @@
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 
+import org.apache.commons.lang.StringUtils;
 import org.apache.ranger.security.context.RangerContextHolder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;
 
+import java.util.Enumeration;
+
 @Component
 public class RestUtil {
 
+	private static final Logger LOG = LoggerFactory.getLogger(RestUtil.class);
 	public static final String timeOffsetCookieName = "clientTimeOffset";
+	public static final String TIMEOUT_ACTION = "timeout";
+	private static final String PROXY_RANGER_URL_PATH = "/ranger";
+	public static final String LOCAL_LOGIN_URL = "locallogin";
 
 	public static Integer getTimeOffset(HttpServletRequest request) {
 		Integer cookieVal = 0;
@@ -74,4 +83,80 @@ public class RestUtil {
 		return clientTimeOffsetInMinute;
 	}
 
+	public static String constructForwardableURL(HttpServletRequest httpRequest) {
+		String xForwardedProto = "";
+		String xForwardedHost = "";
+		String xForwardedContext = "";
+		Enumeration<?> names = httpRequest.getHeaderNames();
+		while (names.hasMoreElements()) {
+			String name = (String) names.nextElement();
+			Enumeration<?> values = httpRequest.getHeaders(name);
+			String value = "";
+			if (values != null) {
+				while (values.hasMoreElements()) {
+					value = (String) values.nextElement();
+				}
+			}
+			if (StringUtils.trimToNull(name) != null && StringUtils.trimToNull(value) != null) {
+				if (name.equalsIgnoreCase("x-forwarded-proto")) {
+					xForwardedProto = value;
+				} else if (name.equalsIgnoreCase("x-forwarded-host")) {
+					xForwardedHost = value;
+				} else if (name.equalsIgnoreCase("x-forwarded-context")) {
+					xForwardedContext = value;
+				}
+			}
+		}
+		if (xForwardedHost.contains(",")) {
+			if (LOG.isDebugEnabled()) {
+				LOG.debug("xForwardedHost value is " + xForwardedHost + " it contains multiple hosts, selecting the first host.");
+			}
+			xForwardedHost = xForwardedHost.split(",")[0].trim();
+		}
+		String xForwardedURL = "";
+		if (StringUtils.trimToNull(xForwardedProto) != null) {
+			//if header contains x-forwarded-host and x-forwarded-context
+			if (StringUtils.trimToNull(xForwardedHost) != null && StringUtils.trimToNull(xForwardedContext) != null) {
+				xForwardedURL = xForwardedProto + "://" + xForwardedHost + xForwardedContext + PROXY_RANGER_URL_PATH + httpRequest.getRequestURI();
+			} else if (StringUtils.trimToNull(xForwardedHost) != null) {
+				//if header contains x-forwarded-host and does not contains x-forwarded-context
+				xForwardedURL = xForwardedProto + "://" + xForwardedHost + httpRequest.getRequestURI();
+			} else {
+				//if header does not contains x-forwarded-host and x-forwarded-context
+				//preserve the x-forwarded-proto value coming from the request.
+				String requestURL = httpRequest.getRequestURL().toString();
+				if (StringUtils.trimToNull(requestURL) != null && requestURL.startsWith("http:")) {
+					requestURL = requestURL.replaceFirst("http", xForwardedProto);
+				}
+				xForwardedURL = requestURL;
+			}
+		}
+		return xForwardedURL;
+	}
+
+	public static String constructRedirectURL(HttpServletRequest request, String redirectUrl, String xForwardedURL, String originalUrlQueryParam) {
+		String delimiter = "?";
+		if (redirectUrl.contains("?")) {
+			delimiter = "&";
+		}
+		String loginURL = redirectUrl + delimiter + originalUrlQueryParam + "=";
+		if (StringUtils.trimToNull(xForwardedURL) != null) {
+			loginURL += xForwardedURL + getOriginalQueryString(request);
+		} else {
+			loginURL += request.getRequestURL().append(getOriginalQueryString(request));
+		}
+		return loginURL;
+	}
+
+	private static String getOriginalQueryString(HttpServletRequest request) {
+		String originalQueryString = request.getQueryString();
+		if (LOG.isDebugEnabled()) {
+			LOG.debug("originalQueryString = " + originalQueryString);
+		}
+		if (originalQueryString == null || originalQueryString.contains("action")) {
+			return "";
+		} else {
+			return "?" + originalQueryString;
+		}
+	}
 }
\ No newline at end of file
diff --git a/security-admin/src/main/java/org/apache/ranger/view/VXPortalUser.java b/security-admin/src/main/java/org/apache/ranger/view/VXPortalUser.java
index 5a1b203..fdb2709 100644
--- a/security-admin/src/main/java/org/apache/ranger/view/VXPortalUser.java
+++ b/security-admin/src/main/java/org/apache/ranger/view/VXPortalUser.java
@@ -21,6 +21,7 @@
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 import javax.xml.bind.annotation.XmlRootElement;
 
@@ -98,6 +99,12 @@ public class VXPortalUser extends VXDataObject implements java.io.Serializable {
 	protected String syncSource;
 
 	/**
+	 * Configuration properties.
+	 *
+	 */
+	protected Map<String, String> configProperties;
+
+	/**
 	 * Default constructor. This will set all the attributes to default value.
 	 */
 	public VXPortalUser ( ) {
@@ -321,6 +328,14 @@ public class VXPortalUser extends VXDataObject implements java.io.Serializable {
 		this.otherAttributes = otherAttributes;
 	}
 
+	public Map<String, String> getConfigProperties() {
+		return configProperties;
+	}
+
+	public void setConfigProperties(Map<String, String> configProperties) {
+		this.configProperties = configProperties;
+	}
+
 	/**
 	 * @return {@link String} - sync Source attribute.
 	 */