com.googlecode.fascinator.portal.services.impl.PortalSecurityManagerImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.googlecode.fascinator.portal.services.impl.PortalSecurityManagerImpl.java

Source

/*
 * The Fascinator - Portal
 * Copyright (C) 2010-2011 University of Southern Queensland
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */
package com.googlecode.fascinator.portal.services.impl;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.RequestGlobals;
import org.apache.tapestry5.services.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.googlecode.fascinator.api.access.AccessControlManager;
import com.googlecode.fascinator.api.authentication.AuthManager;
import com.googlecode.fascinator.api.authentication.AuthenticationException;
import com.googlecode.fascinator.api.authentication.User;
import com.googlecode.fascinator.api.roles.RolesManager;
import com.googlecode.fascinator.common.JsonSimple;
import com.googlecode.fascinator.common.JsonSimpleConfig;
import com.googlecode.fascinator.common.authentication.GenericUser;
import com.googlecode.fascinator.common.authentication.hibernate.HibernateUser;
import com.googlecode.fascinator.common.authentication.hibernate.HibernateUserService;
import com.googlecode.fascinator.portal.FormData;
import com.googlecode.fascinator.portal.JsonSessionState;
import com.googlecode.fascinator.portal.services.PortalManager;
import com.googlecode.fascinator.portal.services.PortalSecurityManager;
import com.googlecode.fascinator.portal.sso.SSOInterface;
import com.googlecode.fascinator.portal.tapestry.TapestryRequestUtil;
import com.googlecode.fascinator.spring.ApplicationContextProvider;

/**
 * The security manager coordinates access to various security plugins when
 * cross plugin awareness is required, and executes some server side logic
 * required for features such as single sign-on.
 * 
 * @author Greg Pendlebury
 */
@Component(value = "portalSecurityManager")
public class PortalSecurityManagerImpl implements PortalSecurityManager {

    /** Prefix for storing SSO parameters whilst round-tripping */
    private static String SSO_STORAGE_PREFIX = "ssoStoredParam_";

    /** Prefix to use for 'source' with trust tokens */
    private static String TRUST_TOKEN_PREFIX = "TrustToken_";

    /** Default trust token expiry period */
    private static String TRUST_TOKEN_EXPIRY = "600";

    /** Logging */
    private Logger log = LoggerFactory.getLogger(PortalSecurityManagerImpl.class);

    /** User entry point for SSO logon */
    private String SSO_LOGIN_PAGE = "/sso";

    /** Form data */
    private FormData formData;

    /** Access Manager - item level security */
    @Inject
    private AccessControlManager accessManager;

    /** Authentication Manager - logging in */
    @Inject
    private AuthManager authManager;

    /** Role Manager - user groups */
    @Resource(name = "fascinatorRoleManager")
    @Inject
    private RolesManager roleManager;

    /** HTTP Request */
    @Inject
    private Request request;

    /** HTTP Response */
    @Inject
    private Response response;

    /** Request globals */
    @Inject
    private RequestGlobals rg;

    /** System Configuration */
    private JsonSimpleConfig config;

    /** Single Sign-On providers */
    private Map<String, SSOInterface> sso;

    /** Server public URL base */
    private String serverUrlBase;

    /** Default Portal */
    private String defaultPortal;

    /** SSO Login URL */
    private String ssoLoginUrl;

    /** detailSubPage detection */
    private Pattern detailPattern;

    /** URL Exclusions : Starts with */
    private List<String> excStarts;

    /** URL Exclusions : Ends with */
    private List<String> excEnds;

    /** URL Exclusions : Equals */
    private List<String> excEquals;

    /** Trust tokens */
    private Map<String, String> tokens;

    /** Trust tokens - Expiry period */
    private Map<String, Long> tokenExpiry;

    /**
     * Basic constructor, should be run automatically by Tapestry.
     * 
     */
    public PortalSecurityManagerImpl() throws IOException {
        // Get system configuration
        config = new JsonSimpleConfig();

        // For all SSO providers configured
        sso = new LinkedHashMap<String, SSOInterface>();
        for (String ssoId : config.getStringList("sso", "plugins")) {
            // Instantiate from the ServiceLoader
            SSOInterface valid = getSSOProvider(ssoId);
            if (valid == null) {
                log.error("Invalid SSO Implementation requested: '{}'", ssoId);
            } else {
                // Store valid implementations
                sso.put(ssoId, valid);
                log.info("SSO Provider instantiated: '{}'", ssoId);
            }
        }

        defaultPortal = config.getString(PortalManager.DEFAULT_PORTAL_NAME, "portal", "defaultView");
        serverUrlBase = config.getString(null, "urlBase");
        ssoLoginUrl = serverUrlBase + defaultPortal + SSO_LOGIN_PAGE;

        // Get exclusions Strings from config
        excStarts = config.getStringList("sso", "urlExclusions", "startsWith");
        excEnds = config.getStringList("sso", "urlExclusions", "endsWith");
        excEquals = config.getStringList("sso", "urlExclusions", "equals");

        // Trust tokens
        Map<String, JsonSimple> tokenMap = config.getJsonSimpleMap("sso", "trustTokens");
        tokens = new HashMap<String, String>();
        tokenExpiry = new HashMap<String, Long>();
        for (String key : tokenMap.keySet()) {
            JsonSimple tok = tokenMap.get(key);
            String publicKey = tok.getString(null, "publicKey");
            String privateKey = tok.getString(null, "privateKey");
            String expiry = tok.getString(TRUST_TOKEN_EXPIRY, "expiry");
            if (publicKey != null && privateKey != null) {
                // Valid key
                tokens.put(publicKey, privateKey);
                tokenExpiry.put(publicKey, Long.valueOf(expiry));
            } else {
                log.error("Invalid token data: '{}'", key);
            }
        }
    }

    /**
     * Get a SSO Provider from the ServiceLoader
     * 
     * @param id SSO Implementation ID
     * @return SSOInterface implementation matching the ID, if found
     */
    private SSOInterface getSSOProvider(String id) {
        ServiceLoader<SSOInterface> providers = ServiceLoader.load(SSOInterface.class);
        for (SSOInterface provider : providers) {
            if (id.equals(provider.getId())) {
                return provider;
            }
        }
        return null;
    }

    /**
     * Return the Access Control Manager
     * 
     * @return AccessControlManager
     */
    @Override
    public AccessControlManager getAccessControlManager() {
        return accessManager;
    }

    /**
     * Return the Authentication Manager
     * 
     * @return AuthManager
     */
    @Override
    public AuthManager getAuthManager() {
        return authManager;
    }

    /**
     * Return the Role Manager
     * 
     * @return RolesManager
     */
    @Override
    public RolesManager getRoleManager() {
        return roleManager;
    }

    /**
     * Get the list of roles possessed by the current user.
     * 
     * @param user The user object of the current user
     * @return String[] A list of roles
     */
    @Override
    public String[] getRolesList(JsonSessionState session, User user) {
        String source = user.getSource();
        List<String> ssoRoles = new ArrayList<String>();

        // SSO Users
        if (sso.containsKey(source)) {
            ssoRoles.addAll(sso.get(source).getRolesList(session));
        }

        // Standard Users
        GenericUser gUser = (GenericUser) user;
        String[] standardRoles = roleManager.getRoles(gUser.getUsername());
        for (String role : standardRoles) {
            // Merge the two
            if (!ssoRoles.contains(role)) {
                ssoRoles.add(role);
            }
        }

        // Cast to array and return
        return ssoRoles.toArray(standardRoles);
    }

    /**
     * Retrieve the details of a user by username
     * 
     * @param username The username of a user to retrieve
     * @param source The authentication source if known
     * @return User The user requested
     * @throws AuthenticationException if any errors occur
     */
    @Override
    public User getUser(JsonSessionState session, String username, String source) throws AuthenticationException {
        // Sanity check
        if (username == null || username.equals("") || source == null || source.equals("")) {
            throw new AuthenticationException("Invalid user data requested");
        }

        // SSO Users
        if (sso.containsKey(source)) {
            GenericUser user = (GenericUser) sso.get(source).getUserObject(session);
            // Sanity check our data
            if (user == null || !user.getUsername().equals(username)) {
                throw new AuthenticationException("Unknown user '" + username + "'");
            }
            return user;
        }

        // Trust token users
        if (source.startsWith(TRUST_TOKEN_PREFIX)) {
            String sUsername = (String) session.get("username");
            String sSource = (String) session.get("source");

            // We can't lookup token users so it must match
            if (sUsername == null || !username.equals(sUsername) || sSource == null || !source.equals(sSource)) {
                throw new AuthenticationException("Unknown user '" + username + "'");
            }

            // Seems valid, create a basic user object and return
            GenericUser user = new GenericUser();
            user.setUsername(username);
            user.setSource(source);
            return user;
        }

        // Standard users
        authManager.setActivePlugin(source);
        return authManager.getUser(username);
    }

    /**
     * Logout the provided user
     * 
     * @return user The user to logout
     */
    @Override
    public void logout(JsonSessionState session, User user) throws AuthenticationException {
        String source = user.getSource();

        // Clear session
        session.remove("username");
        session.remove("source");

        // SSO Users
        if (sso.containsKey(source)) {
            sso.get(source).logout(session);
            return;
        }

        // Trust token users
        if (source.startsWith(TRUST_TOKEN_PREFIX)) {
            session.remove("validToken");
            return;
        }

        // Standard users
        authManager.logOut(user);
    }

    /**
     * Wrapper method for other SSO methods provided by the security manager. If
     * desired, the security manager can take care of the integration using the
     * default usage pattern, rather then calling them individually.
     * 
     * @param session : The session of the current request
     * @param formData : FormData object for the current request
     * @return boolean : True if SSO has redirected, in which case no response
     *         should be sent by Dispatch, otherwise False.
     */
    @Override
    public boolean runSsoIntegration(JsonSessionState session, FormData formData) {
        this.formData = formData;

        // Used in integrating with thrid party systems. They can send us a
        // user, we will log them in via a SSO round-trip, then send them back
        // to the external system
        String returnUrl = request.getParameter("returnUrl");
        if (returnUrl != null) {
            log.info("External redirect requested: '{}'", returnUrl);
            session.set("ssoReturnUrl", returnUrl);
        }

        // The URL parameters can contain a trust token
        String utoken = request.getParameter("token");
        String stoken = (String) session.get("validToken");
        String token = null;
        // Or an 'old' token still in the session
        if (stoken != null) {
            token = stoken;
        }
        // But give the URL priority
        if (utoken != null) {
            token = utoken;
        }
        if (token != null) {
            // Valid token
            if (testTrustToken(session, token)) {
                // Dispatch can continue
                return false;
            }

            // Invalid token
            // Given that trust tokens are designed for system integration
            // we are going to fail with a non-branded error message
            try {
                response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid or expired security token!");
            } catch (IOException ex) {
                log.error("Error sending 403 response to client!");
            }
            // We don't want Dispatch to send a response
            return true;
        }

        // Single Sign-On integration
        try {
            // Instantiate with access to the session
            String ssoId = ssoInit(session);
            if (ssoId != null) {
                // We are logging in, so send them to the SSO portal
                String ssoUrl = ssoGetRemoteLogonURL(session, ssoId);
                if (ssoUrl != null) {
                    log.info("Redirect to external URL: '{}'", ssoUrl);
                    response.sendRedirect(ssoUrl);
                    return true;
                }
            } else {
                // Otherwise, check if we have user's details
                boolean valid = ssoCheckUserDetails(session);
                // If we validly logged in an SSO user, check for an
                // external redirect to third party systems
                if (valid) {
                    returnUrl = (String) session.get("ssoReturnUrl");
                    if (returnUrl != null) {
                        log.info("Redirect to external URL: '{}'", returnUrl);
                        session.remove("ssoReturnUrl");
                        response.sendRedirect(returnUrl);
                        return true;
                    }
                }
            }
        } catch (Exception ex) {
            log.error("SSO Error!", ex);
        }

        return false;
    }

    /**
     * Initialize the SSO Service, prepare a login if required
     * 
     * @param session The server session data
     * @throws Exception if any errors occur
     */
    @Override
    public String ssoInit(JsonSessionState session) throws Exception {
        // Keep track of the user switching portals for
        // link building in other methods
        String portalId = (String) session.get("portalId", defaultPortal);
        ssoLoginUrl = serverUrlBase + portalId + SSO_LOGIN_PAGE;

        // Find out what page we are on
        String path = request.getAttribute("RequestURI").toString();
        String currentAddress = serverUrlBase + path;
        if (!StringUtils.isEmpty(TapestryRequestUtil.getQueryString(request))) {
            currentAddress = currentAddress + "?" + TapestryRequestUtil.getQueryString(request);
        }
        // Store the portal URL, might be required by implementers to build
        // an interface (images etc).
        session.set("ssoPortalUrl", serverUrlBase + portalId);

        // Makes sure all SSO plugins get initialised
        for (String ssoId : sso.keySet()) {
            sso.get(ssoId).ssoInit(session, rg.getHTTPServletRequest());
        }

        // Are we logging in right now?
        String ssoId = request.getParameter("ssoId");

        // If this isn't the login page...
        if (!currentAddress.contains(SSO_LOGIN_PAGE)) {
            // Store the current address for use later
            session.set("returnAddress", currentAddress);
            // We might still be logging in from a deep link
            if (ssoId == null) {
                // No we're not, finished now
                return null;
            } else {
                // Yes it's a deep link, store any extra query params
                // since they probably won't survive the round-trip
                // through SSO.
                for (String param : request.getParameterNames()) {
                    if (!param.equals("ssoId")) {
                        // Store all the other parameters
                        session.set(SSO_STORAGE_PREFIX + param, request.getParameter(param));
                    }
                }
            }
        }

        // Get the last address to return the user to
        String returnAddress = (String) session.get("returnAddress");
        if (returnAddress == null) {
            // Or use the home page
            returnAddress = serverUrlBase + portalId + "/home";
        }

        // Which SSO provider did the user request?
        if (ssoId == null) {
            log.error("==== SSO: SSO ID not found!");
            return null;
        }
        if (!sso.containsKey(ssoId)) {
            log.error("==== SSO: SSO ID invalid: '{}'!", ssoId);
            return null;
        }

        // The main event... finally
        sso.get(ssoId).ssoPrepareLogin(session, returnAddress, serverUrlBase);
        return ssoId;
    }

    /**
     * Get user details from SSO connection and set them in the user session.
     * 
     * @return boolean: Flag whether a user was actually logged in or not.
     */
    @Override
    public boolean ssoCheckUserDetails(JsonSessionState session) {
        // After the SSO roun-trip, restore any old query parameters we lost
        List<String> currentParams = request.getParameterNames();
        // Cast a copy of keySet() to array to avoid errors as we modify
        String[] oldParams = session.keySet().toArray(new String[0]);
        // Loop through session data...
        for (String key : oldParams) {
            // ... looking for SSO stored params
            if (key.startsWith(SSO_STORAGE_PREFIX)) {
                // Remove our prefix...
                String oldParam = key.replace(SSO_STORAGE_PREFIX, "");
                // ... and check if it survived the trip
                if (!currentParams.contains(oldParam)) {
                    // No it didn't, add it to form data... the parameters are
                    // already accessible from there in Jython
                    String data = (String) session.get(key);
                    formData.set(oldParam, data);
                    // Don't forget to clear it from the session
                    session.remove(key);
                }
            }
        }

        // Check our SSO providers for valid logins
        for (String ssoId : sso.keySet()) {
            sso.get(ssoId).ssoCheckUserDetails(session);
            GenericUser user = (GenericUser) sso.get(ssoId).getUserObject(session);
            if (user != null) {
                session.set("username", user.getUsername());
                session.set("source", ssoId);
                HibernateUserService hibernateAuthUserService = (HibernateUserService) ApplicationContextProvider
                        .getApplicationContext().getBean("hibernateAuthUserService");
                log.debug("Auth manager adding user through hibernate...");
                hibernateAuthUserService.addUser(new HibernateUser(user));
                return true;
            }
        }
        return false;
    }

    /**
     * Build a Map of Maps of on-screen string values for each SSO provider.
     * Should be enough to generate a login interface.
     * 
     * @return Map Containing the data structure of valid SSO interfaces.
     */
    @Override
    public Map<String, Map<String, String>> ssoBuildLogonInterface(JsonSessionState session) {
        Map<String, Map<String, String>> ssoInterface = new LinkedHashMap<String, Map<String, String>>();
        for (String ssoId : sso.keySet()) {
            SSOInterface provider = sso.get(ssoId);
            Map<String, String> map = new HashMap<String, String>();
            map.put("label", provider.getLabel());
            map.put("interface", provider.getInterface(ssoLoginUrl + "?ssoId=" + ssoId));
            ssoInterface.put(ssoId, map);
        }
        return ssoInterface;
    }

    /**
     * Retrieve the login URL for redirection against a given provider.
     * 
     * @param String The SSO source to use
     * @return String The URL used by the SSO Service for logins
     */
    public String ssoGetRemoteLogoutURL(JsonSessionState session, String source) {
        if (!sso.containsKey(source)) {
            return null;
        } else {
            // return sso.get(source).ssoGetRemoteLogonURL(session);
            Class ssoInterfaceClass = sso.get(source).getClass();
            Method remoteLogoutURLMethod;
            try {
                remoteLogoutURLMethod = ssoInterfaceClass.getMethod("ssoGetRemoteLogoutURL",
                        JsonSessionState.class);
                if (remoteLogoutURLMethod != null) {
                    return (String) remoteLogoutURLMethod.invoke(sso.get(source), session);
                }
            } catch (NoSuchMethodException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (SecurityException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            return null;
        }
    }

    /**
     * Retrieve the login URL for redirection against a given provider.
     * 
     * @param String The SSO source to use
     * @return String The URL used by the SSO Service for logins
     */
    @Override
    public String ssoGetRemoteLogonURL(JsonSessionState session, String source) {
        if (!sso.containsKey(source)) {
            return null;
        } else {
            return sso.get(source).ssoGetRemoteLogonURL(session);
        }
    }

    /**
     * Given the provided resource, test whether SSO should be 'aware' of this
     * resource. 'Aware' resources are valid return return points after SSO
     * redirects, so the test should return false on (for examples) static
     * resources and utilities such as atom feeds.
     * 
     * @param session : The session for this request
     * @param resource : The name of the resource being accessed
     * @param uri : The full URI of the resource if simple matches fail
     * @return boolean : True if SSO should be evaluated, False otherwise
     */
    @Override
    public boolean testForSso(JsonSessionState session, String resource, String uri) {
        // The URL parameters can request forced SSO to this URL
        String ssoId = request.getParameter("ssoId");
        if (ssoId != null) {
            return true;
        }

        // The URL parameters can contain a trust token
        String utoken = request.getParameter("token");
        String stoken = (String) session.get("validToken");
        if (utoken != null || stoken != null) {
            return true;
        }

        // Test for resources that start with unwanted values
        for (String test : excStarts) {
            if (resource.startsWith(test)) {
                return false;
            }
        }

        // Test for resources that end with unwanted values
        for (String test : excEnds) {
            if (resource.endsWith(test)) {
                return false;
            }
        }

        // Test for resources that equal unwanted values
        for (String test : excEquals) {
            if (resource.equals(test)) {
                return false;
            }
        }

        // Detail screen - specific payload target
        // This is an edge case, where the payload was a deep link,
        // it's not a subpage we can ignore
        String returnAddress = (String) session.get("returnAddress");
        if (returnAddress != null && returnAddress.endsWith(uri)) {
            return true;
        }

        // The detail screen generates a lot of background calls to the server
        if (resource.equals("detail") || resource.equals("download") || resource.equals("preview")) {
            // Now check for the core page
            if (resource.equals("detail")) {
                if (detailPattern == null) {
                    detailPattern = Pattern.compile("detail/\\w+/*$");
                }
                Matcher matcher = detailPattern.matcher(uri);
                if (matcher.find()) {
                    // This is actually the 'core' detail page
                    return true;
                }
            }

            // This is just a subpage
            return false;
        }

        // Every other page
        return true;
    }

    /**
     * Validate the provided trust token.
     * 
     * @param token : The token to validate
     * @return boolean : True if the token is valid, False otherwise
     */
    @Override
    public boolean testTrustToken(JsonSessionState session, String token) {
        String[] parts = StringUtils.split(token, ":");

        // Check the length
        if (parts.length != 4) {
            log.error("TOKEN: Should have 4 parts, not {} : '{}'", parts.length, token);
            return false;
        }

        // Check the parts
        String username = parts[0];
        String timestamp = parts[1];
        String publicKey = parts[2];
        String userToken = parts[3];
        if (username.isEmpty() || timestamp.isEmpty() || publicKey.isEmpty() || userToken.isEmpty()) {
            log.error("TOKEN: One or more parts are empty : '{}'", token);
            return false;
        }

        // Make sure the publicKey is valid
        if (!tokens.containsKey(publicKey)) {
            log.error("TOKEN: Invalid public key : '{}'", publicKey);
            return false;
        }
        String privateKey = tokens.get(publicKey);
        Long expiry = tokenExpiry.get(publicKey);

        // Check for valid timestamp & expiry
        timestamp = getFormattedTime(timestamp);
        if (timestamp == null) {
            log.error("TOKEN: Invalid timestamp : '{}'", timestamp);
            return false;
        }
        Long tokenTime = Long.valueOf(timestamp);
        Long currentTime = Long.valueOf(getFormattedTime(null));
        Long age = currentTime - tokenTime;
        if (age > expiry) {
            log.error("Token is passed its expiry : {}s old", age);
            return false;
        }

        // Now validate the token itself
        String tokenSeed = username + ":" + timestamp + ":" + privateKey;
        String expectedToken = DigestUtils.md5Hex(tokenSeed);
        if (userToken.equals(expectedToken)) {
            // The token is valid
            session.set("username", username);
            session.set("source", TRUST_TOKEN_PREFIX + publicKey);
            // Store it in case we redirect later
            session.set("validToken", token);
            return true;
        }

        // Token was not valid
        log.error("TOKEN: Invalid token, hash does not match: '{}'", userToken);
        return false;
    }

    /**
     * Get (or validate) a formatted time string. If the input is null, the
     * current time will be returned, otherwise it will validate the provided
     * string, returning null if it is invalid.
     * 
     * @param input : A time string to validate, null will use current time
     * @return String : A formatted time string, null if input is invalid
     */
    private String getFormattedTime(String input) {
        DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
        Date dateData;
        if (input == null) {
            // Now
            dateData = new Date();
        } else {
            try {
                // Parse provided date
                dateData = dateFormat.parse(input);
            } catch (ParseException ex) {
                // Invalid date provided
                return null;
            }
        }

        // Return a long containing the time
        return dateFormat.format(dateData);
    }
}