shiro-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From lhazlew...@apache.org
Subject svn commit: r1226983 - in /shiro/trunk/support: ./ cas/ cas/src/ cas/src/main/ cas/src/main/java/ cas/src/main/java/org/ cas/src/main/java/org/apache/ cas/src/main/java/org/apache/shiro/ cas/src/main/java/org/apache/shiro/cas/ cas/src/test/ cas/src/tes...
Date Tue, 03 Jan 2012 23:12:50 GMT
Author: lhazlewood
Date: Tue Jan  3 23:12:49 2012
New Revision: 1226983

URL: http://svn.apache.org/viewvc?rev=1226983&view=rev
Log:
SHIRO-285: Initial CAS support implementations.

Added:
    shiro/trunk/support/cas/   (with props)
    shiro/trunk/support/cas/pom.xml
    shiro/trunk/support/cas/src/
    shiro/trunk/support/cas/src/main/
    shiro/trunk/support/cas/src/main/java/
    shiro/trunk/support/cas/src/main/java/org/
    shiro/trunk/support/cas/src/main/java/org/apache/
    shiro/trunk/support/cas/src/main/java/org/apache/shiro/
    shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/
    shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasAuthenticationException.java
    shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasFilter.java
    shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasRealm.java
    shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasRememberMeSecurityManager.java
    shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasSubjectFactory.java
    shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasToken.java
    shiro/trunk/support/cas/src/test/
    shiro/trunk/support/cas/src/test/groovy/
    shiro/trunk/support/cas/src/test/groovy/org/
    shiro/trunk/support/cas/src/test/groovy/org/apache/
    shiro/trunk/support/cas/src/test/groovy/org/apache/shiro/
    shiro/trunk/support/cas/src/test/groovy/org/apache/shiro/cas/
    shiro/trunk/support/cas/src/test/groovy/org/apache/shiro/cas/CasRealmTest.groovy
    shiro/trunk/support/cas/src/test/groovy/org/apache/shiro/cas/CasTokenTest.groovy
    shiro/trunk/support/cas/src/test/groovy/org/apache/shiro/cas/MockServiceTicketValidator.groovy
Modified:
    shiro/trunk/support/pom.xml

Propchange: shiro/trunk/support/cas/
------------------------------------------------------------------------------
--- svn:ignore (added)
+++ svn:ignore Tue Jan  3 23:12:49 2012
@@ -0,0 +1 @@
+*.iml

Added: shiro/trunk/support/cas/pom.xml
URL: http://svn.apache.org/viewvc/shiro/trunk/support/cas/pom.xml?rev=1226983&view=auto
==============================================================================
--- shiro/trunk/support/cas/pom.xml (added)
+++ shiro/trunk/support/cas/pom.xml Tue Jan  3 23:12:49 2012
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <parent>
+        <groupId>org.apache.shiro</groupId>
+        <artifactId>shiro-root</artifactId>
+        <version>1.2.0-SNAPSHOT</version>
+        <relativePath>../../pom.xml</relativePath>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>shiro-cas</artifactId>
+    <name>Apache Shiro :: Support :: CAS</name>
+    <packaging>bundle</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.shiro</groupId>
+            <artifactId>shiro-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.jasig.cas.client</groupId>
+            <artifactId>cas-client-core</artifactId>
+            <version>3.2.1</version>
+        </dependency>
+        <!-- for SAML ticket validation -->
+        <dependency>
+            <groupId>org.opensaml</groupId>
+            <artifactId>opensaml</artifactId>
+            <version>1.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.santuario</groupId>
+            <artifactId>xmlsec</artifactId>
+            <version>1.4.3</version>
+        </dependency>
+        <!-- for SAML ticket validation -->
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Bundle-SymbolicName>org.apache.shiro.cas</Bundle-SymbolicName>
+                        <Export-Package>org.apache.shiro.cas*;version=${project.version}</Export-Package>
+                        <Import-Package>
+                            org.apache.shiro*;version="${shiro.osgi.importRange}",
+                            org.jasig.cas*;version="[3.1.3, 4)",
+                            *
+                        </Import-Package>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

Added: shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasAuthenticationException.java
URL: http://svn.apache.org/viewvc/shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasAuthenticationException.java?rev=1226983&view=auto
==============================================================================
--- shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasAuthenticationException.java (added)
+++ shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasAuthenticationException.java Tue Jan  3 23:12:49 2012
@@ -0,0 +1,43 @@
+/*
+ * 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.shiro.cas;
+
+import org.apache.shiro.authc.AuthenticationException;
+
+/**
+ * @since 1.2
+ */
+public class CasAuthenticationException extends AuthenticationException {
+
+    public CasAuthenticationException() {
+        super();
+    }
+
+    public CasAuthenticationException(String message) {
+        super(message);
+    }
+
+    public CasAuthenticationException(Throwable cause) {
+        super(cause);
+    }
+
+    public CasAuthenticationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

Added: shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasFilter.java
URL: http://svn.apache.org/viewvc/shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasFilter.java?rev=1226983&view=auto
==============================================================================
--- shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasFilter.java (added)
+++ shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasFilter.java Tue Jan  3 23:12:49 2012
@@ -0,0 +1,150 @@
+/*
+ * 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.shiro.cas;
+
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
+import org.apache.shiro.web.util.WebUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+
+/**
+ * This filter validates the CAS service ticket to authenticate the user.  It must be configured on the URL recognized
+ * by the CAS server.  For example, in {@code shiro.ini}:
+ * <pre>
+ * [main]
+ * casFilter = org.apache.shiro.cas.CasFilter
+ * ...
+ *
+ * [urls]
+ * /shiro-cas = casFilter
+ * ...
+ * </pre>
+ * (example : http://host:port/mycontextpath/shiro-cas)
+ *
+ * @since 1.2
+ */
+public class CasFilter extends AuthenticatingFilter {
+    
+    private static Logger logger = LoggerFactory.getLogger(CasFilter.class);
+    
+    // the name of the parameter service ticket in url
+    private static final String TICKET_PARAMETER = "ticket";
+    
+    // the url where the application is redirected if the CAS service ticket validation failed (example : /mycontextpatch/cas_error.jsp)
+    private String failureUrl;
+    
+    /**
+     * The token created for this authentication is a CasToken containing the CAS service ticket received on the CAS service url (on which
+     * the filter must be configured).
+     * 
+     * @param request the incoming request
+     * @param response the outgoing response
+     * @throws Exception if there is an error processing the request.
+     */
+    @Override
+    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        String ticket = httpRequest.getParameter(TICKET_PARAMETER);
+        return new CasToken(ticket);
+    }
+    
+    /**
+     * Execute login by creating {@link #createToken(javax.servlet.ServletRequest, javax.servlet.ServletResponse) token} and logging subject
+     * with this token.
+     * 
+     * @param request the incoming request
+     * @param response the outgoing response
+     * @throws Exception if there is an error processing the request.
+     */
+    @Override
+    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
+        return executeLogin(request, response);
+    }
+    
+    /**
+     * Returns <code>false</code> to always force authentication (user is never considered authenticated by this filter).
+     * 
+     * @param request the incoming request
+     * @param response the outgoing response
+     * @param mappedValue the filter-specific config value mapped to this filter in the URL rules mappings.
+     * @return <code>false</code>
+     */
+    @Override
+    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
+        return false;
+    }
+    
+    /**
+     * If login has been successful, redirect user to the original protected url.
+     * 
+     * @param token the token representing the current authentication
+     * @param subject the current authenticated subjet
+     * @param request the incoming request
+     * @param response the outgoing response
+     * @throws Exception if there is an error processing the request.
+     */
+    @Override
+    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,
+                                     ServletResponse response) throws Exception {
+        issueSuccessRedirect(request, response);
+        return false;
+    }
+    
+    /**
+     * If login has failed, redirect user to the CAS error page (no ticket or ticket validation failed) except if the user is already
+     * authenticated, in which case redirect to the default success url.
+     * 
+     * @param token the token representing the current authentication
+     * @param ae the current authentication exception
+     * @param request the incoming request
+     * @param response the outgoing response
+     */
+    @Override
+    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException ae, ServletRequest request,
+                                     ServletResponse response) {
+        // is user authenticated or in remember me mode ?
+        Subject subject = getSubject(request, response);
+        if (subject.isAuthenticated() || subject.isRemembered()) {
+            try {
+                issueSuccessRedirect(request, response);
+            } catch (Exception e) {
+                logger.error("Cannot redirect to the default success url", e);
+            }
+        } else {
+            try {
+                WebUtils.issueRedirect(request, response, failureUrl);
+            } catch (IOException e) {
+                logger.error("Cannot redirect to failure url : {}", failureUrl, e);
+            }
+        }
+        return false;
+    }
+    
+    public void setFailureUrl(String failureUrl) {
+        this.failureUrl = failureUrl;
+    }
+}

Added: shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasRealm.java
URL: http://svn.apache.org/viewvc/shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasRealm.java?rev=1226983&view=auto
==============================================================================
--- shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasRealm.java (added)
+++ shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasRealm.java Tue Jan  3 23:12:49 2012
@@ -0,0 +1,304 @@
+/*
+ * 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.shiro.cas;
+
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.authc.SimpleAuthenticationInfo;
+import org.apache.shiro.authz.AuthorizationInfo;
+import org.apache.shiro.authz.SimpleAuthorizationInfo;
+import org.apache.shiro.realm.AuthorizingRealm;
+import org.apache.shiro.subject.PrincipalCollection;
+import org.apache.shiro.subject.SimplePrincipalCollection;
+import org.apache.shiro.util.CollectionUtils;
+import org.apache.shiro.util.StringUtils;
+import org.jasig.cas.client.authentication.AttributePrincipal;
+import org.jasig.cas.client.validation.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This realm implementation acts as a CAS client to a CAS server for authentication and basic authorization.
+ * <p/>
+ * This realm functions by inspecting a submitted {@link org.apache.shiro.cas.CasToken CasToken} (which essentially 
+ * wraps a CAS service ticket) and validates it against the CAS server using a configured CAS
+ * {@link org.jasig.cas.client.validation.TicketValidator TicketValidator}.
+ * <p/>
+ * The {@link #getValidationProtocol() validationProtocol} is {@code CAS} by default, which indicates that a
+ * a {@link org.jasig.cas.client.validation.Cas20ServiceTicketValidator Cas20ServiceTicketValidator}
+ * will be used for ticket validation.  You can alternatively set
+ * or {@link org.jasig.cas.client.validation.Saml11TicketValidator Saml11TicketValidator} of CAS client. It is based on
+ * {@link AuthorizingRealm AuthorizingRealm} for both authentication and authorization. User id and attributes are retrieved from the CAS
+ * service ticket validation response during authentication phase. Roles and permissions are computed during authorization phase (according
+ * to the attributes previously retrieved).
+ *
+ * @since 1.2
+ */
+public class CasRealm extends AuthorizingRealm {
+
+    // default name of the CAS attribute for remember me authentication (CAS 3.4.10+)
+    public static final String DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME = "longTermAuthenticationRequestTokenUsed";
+    public static final String DEFAULT_VALIDATION_PROTOCOL = "CAS";
+    
+    private static Logger log = LoggerFactory.getLogger(CasRealm.class);
+    
+    // this is the url of the CAS server (example : http://host:port/cas)
+    private String casServerUrlPrefix;
+    
+    // this is the CAS service url of the application (example : http://host:port/mycontextpath/shiro-cas)
+    private String casService;
+    
+    /* CAS protocol to use for ticket validation : CAS (default) or SAML :
+       - CAS protocol can be used with CAS server version < 3.1 : in this case, no user attributes can be retrieved from the CAS ticket validation response (except if there are some customizations on CAS server side)
+       - SAML protocol can be used with CAS server version >= 3.1 : in this case, user attributes can be extracted from the CAS ticket validation response
+    */
+    private String validationProtocol = DEFAULT_VALIDATION_PROTOCOL;
+    
+    // default name of the CAS attribute for remember me authentication (CAS 3.4.10+)
+    private String rememberMeAttributeName = DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME;
+    
+    // this class from the CAS client is used to validate a service ticket on CAS server
+    private TicketValidator ticketValidator;
+    
+    // default roles to applied to authenticated user
+    private String defaultRoles;
+    
+    // default permissions to applied to authenticated user
+    private String defaultPermissions;
+    
+    // names of attributes containing roles
+    private String roleAttributeNames;
+    
+    // names of attributes containing permissions
+    private String permissionAttributeNames;
+    
+    public CasRealm() {
+        setAuthenticationTokenClass(CasToken.class);
+    }
+    
+    protected TicketValidator ensureTicketValidator() {
+        if (this.ticketValidator == null) {
+            this.ticketValidator = createTicketValidator();
+        }
+        return this.ticketValidator;
+    }
+    
+    protected TicketValidator createTicketValidator() {
+        String urlPrefix = getCasServerUrlPrefix();
+        if ("saml".equalsIgnoreCase(getValidationProtocol())) {
+            return new Saml11TicketValidator(urlPrefix);
+        }
+        return new Cas20ServiceTicketValidator(urlPrefix);
+    }
+    
+    /**
+     * Authenticates a user and retrieves its information.
+     * 
+     * @param token the authentication token
+     * @throws AuthenticationException if there is an error during authentication.
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
+        CasToken casToken = (CasToken) token;
+        if (token == null) {
+            return null;
+        }
+        
+        String ticket = (String)casToken.getCredentials();
+        if (!StringUtils.hasText(ticket)) {
+            return null;
+        }
+        
+        TicketValidator ticketValidator = ensureTicketValidator();
+
+        try {
+            // contact CAS server to validate service ticket
+            Assertion casAssertion = ticketValidator.validate(ticket, getCasService());
+            // get principal, user id and attributes
+            AttributePrincipal casPrincipal = casAssertion.getPrincipal();
+            String userId = casPrincipal.getName();
+            log.debug("Validate ticket : {} in CAS server : {} to retrieve user : {}", new Object[]{
+                    ticket, getCasServerUrlPrefix(), userId
+            });
+
+            Map<String, Object> attributes = casPrincipal.getAttributes();
+            // refresh authentication token (user id + remember me)
+            casToken.setUserId(userId);
+            String rememberMeAttributeName = getRememberMeAttributeName();
+            String rememberMeStringValue = (String)attributes.get(rememberMeAttributeName);
+            boolean isRemembered = rememberMeStringValue != null && Boolean.parseBoolean(rememberMeStringValue);
+            if (isRemembered) {
+                casToken.setRememberMe(true);
+            }
+            // create simple authentication info
+            List<Object> principals = CollectionUtils.asList(userId, attributes);
+            PrincipalCollection principalCollection = new SimplePrincipalCollection(principals, getName());
+            return new SimpleAuthenticationInfo(principalCollection, ticket);
+        } catch (TicketValidationException e) { 
+            throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e);
+        }
+    }
+    
+    /**
+     * Retrieves the AuthorizationInfo for the given principals (the CAS previously authenticated user : id + attributes).
+     * 
+     * @param principals the primary identifying principals of the AuthorizationInfo that should be retrieved.
+     * @return the AuthorizationInfo associated with this principals.
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
+        // retrieve user information
+        SimplePrincipalCollection principalCollection = (SimplePrincipalCollection) principals;
+        List<Object> listPrincipals = principalCollection.asList();
+        Map<String, String> attributes = (Map<String, String>) listPrincipals.get(1);
+        // create simple authorization info
+        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
+        // add default roles
+        addRoles(simpleAuthorizationInfo, split(defaultRoles));
+        // add default permissions
+        addPermissions(simpleAuthorizationInfo, split(defaultPermissions));
+        // get roles from attributes
+        List<String> attributeNames = split(roleAttributeNames);
+        for (String attributeName : attributeNames) {
+            String value = attributes.get(attributeName);
+            addRoles(simpleAuthorizationInfo, split(value));
+        }
+        // get permissions from attributes
+        attributeNames = split(permissionAttributeNames);
+        for (String attributeName : attributeNames) {
+            String value = attributes.get(attributeName);
+            addPermissions(simpleAuthorizationInfo, split(value));
+        }
+        return simpleAuthorizationInfo;
+    }
+    
+    /**
+     * Split a string into a list of not empty and trimmed strings, delimiter is a comma.
+     * 
+     * @param s the input string
+     * @return the list of not empty and trimmed strings
+     */
+    private List<String> split(String s) {
+        List<String> list = new ArrayList<String>();
+        String[] elements = StringUtils.split(s, ',');
+        if (elements != null && elements.length > 0) {
+            for (String element : elements) {
+                if (StringUtils.hasText(element)) {
+                    list.add(element.trim());
+                }
+            }
+        }
+        return list;
+    }
+    
+    /**
+     * Add roles to the simple authorization info.
+     * 
+     * @param simpleAuthorizationInfo
+     * @param roles the list of roles to add
+     */
+    private void addRoles(SimpleAuthorizationInfo simpleAuthorizationInfo, List<String> roles) {
+        for (String role : roles) {
+            simpleAuthorizationInfo.addRole(role);
+        }
+    }
+    
+    /**
+     * Add permissions to the simple authorization info.
+     * 
+     * @param simpleAuthorizationInfo
+     * @param permissions the list of permissions to add
+     */
+    private void addPermissions(SimpleAuthorizationInfo simpleAuthorizationInfo, List<String> permissions) {
+        for (String permission : permissions) {
+            simpleAuthorizationInfo.addStringPermission(permission);
+        }
+    }
+
+    public String getCasServerUrlPrefix() {
+        return casServerUrlPrefix;
+    }
+
+    public void setCasServerUrlPrefix(String casServerUrlPrefix) {
+        this.casServerUrlPrefix = casServerUrlPrefix;
+    }
+
+    public String getCasService() {
+        return casService;
+    }
+
+    public void setCasService(String casService) {
+        this.casService = casService;
+    }
+
+    public String getValidationProtocol() {
+        return validationProtocol;
+    }
+
+    public void setValidationProtocol(String validationProtocol) {
+        this.validationProtocol = validationProtocol;
+    }
+
+    public String getRememberMeAttributeName() {
+        return rememberMeAttributeName;
+    }
+
+    public void setRememberMeAttributeName(String rememberMeAttributeName) {
+        this.rememberMeAttributeName = rememberMeAttributeName;
+    }
+
+    public String getDefaultRoles() {
+        return defaultRoles;
+    }
+
+    public void setDefaultRoles(String defaultRoles) {
+        this.defaultRoles = defaultRoles;
+    }
+
+    public String getDefaultPermissions() {
+        return defaultPermissions;
+    }
+
+    public void setDefaultPermissions(String defaultPermissions) {
+        this.defaultPermissions = defaultPermissions;
+    }
+
+    public String getRoleAttributeNames() {
+        return roleAttributeNames;
+    }
+
+    public void setRoleAttributeNames(String roleAttributeNames) {
+        this.roleAttributeNames = roleAttributeNames;
+    }
+
+    public String getPermissionAttributeNames() {
+        return permissionAttributeNames;
+    }
+
+    public void setPermissionAttributeNames(String permissionAttributeNames) {
+        this.permissionAttributeNames = permissionAttributeNames;
+    }
+}

Added: shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasRememberMeSecurityManager.java
URL: http://svn.apache.org/viewvc/shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasRememberMeSecurityManager.java?rev=1226983&view=auto
==============================================================================
--- shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasRememberMeSecurityManager.java (added)
+++ shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasRememberMeSecurityManager.java Tue Jan  3 23:12:49 2012
@@ -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.shiro.cas;
+
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.subject.SubjectContext;
+import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
+
+/**
+ * This security manager is specifically dedicated to CAS authentication. Remember me is not managed by RememberMeManager but during context
+ * initialization in subject creation.
+ */
+public class CasRememberMeSecurityManager extends DefaultWebSecurityManager {
+    
+    /**
+     * Construct the security manager for CAS. The manager for remember me is explicitly set to null : no {@code RememberMeManager} is
+     * required for CAS.
+     */
+    public CasRememberMeSecurityManager() {
+        setRememberMeManager(null);
+    }
+    
+    /**
+     * Creates a {@code Subject} instance for the user represented by the given method arguments. As a CAS authentication, the authenticated
+     * flag is computed according to the level of authentication : login/password authentication or remember me mode.
+     * 
+     * @param token the {@code AuthenticationToken} submitted for the successful authentication.
+     * @param info the {@code AuthenticationInfo} of a newly authenticated user.
+     * @param existing the existing {@code Subject} instance that initiated the authentication attempt
+     * @return the {@code Subject} instance that represents the context and session data for the newly authenticated subject.
+     */
+    @Override
+    protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
+        SubjectContext context = createSubjectContext();
+        // set the authenticated flag of the context to true only if the CAS subject is not in a remember me mode
+        if (token != null && token instanceof CasToken) {
+            CasToken casToken = (CasToken) token;
+            if (casToken.isRememberMe()) {
+                context.setAuthenticated(false);
+            } else {
+                context.setAuthenticated(true);
+            }
+        }
+        context.setAuthenticationToken(token);
+        context.setAuthenticationInfo(info);
+        if (existing != null) {
+            context.setSubject(existing);
+        }
+        return createSubject(context);
+    }
+}

Added: shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasSubjectFactory.java
URL: http://svn.apache.org/viewvc/shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasSubjectFactory.java?rev=1226983&view=auto
==============================================================================
--- shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasSubjectFactory.java (added)
+++ shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasSubjectFactory.java Tue Jan  3 23:12:49 2012
@@ -0,0 +1,56 @@
+/*
+ * 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.shiro.cas;
+
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.subject.SubjectContext;
+import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;
+
+/**
+ * {@link org.apache.shiro.mgt.SubjectFactory Subject} implementation to be used in CAS-enabled applications.
+ *
+ * @since 1.2
+ */
+public class CasSubjectFactory extends DefaultWebSubjectFactory {
+
+    @Override
+    public Subject createSubject(SubjectContext context) {
+
+        //the authenticated flag is only set by the SecurityManager after a successful authentication attempt.
+        boolean authenticated = context.isAuthenticated();
+
+        //although the SecurityManager 'sees' the submission as a successful authentication, in reality, the
+        //login might have been just a CAS rememberMe login.  If so, set the authenticated flag appropriately:
+        if (authenticated) {
+
+            AuthenticationToken token = context.getAuthenticationToken();
+
+            if (token != null && token instanceof CasToken) {
+                CasToken casToken = (CasToken) token;
+                // set the authenticated flag of the context to true only if the CAS subject is not in a remember me mode
+                if (casToken.isRememberMe()) {
+                    context.setAuthenticated(false);
+                }
+            }
+        }
+
+        return super.createSubject(context);
+    }
+}

Added: shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasToken.java
URL: http://svn.apache.org/viewvc/shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasToken.java?rev=1226983&view=auto
==============================================================================
--- shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasToken.java (added)
+++ shiro/trunk/support/cas/src/main/java/org/apache/shiro/cas/CasToken.java Tue Jan  3 23:12:49 2012
@@ -0,0 +1,46 @@
+package org.apache.shiro.cas;
+
+import org.apache.shiro.authc.RememberMeAuthenticationToken;
+
+/**
+ * This class represents a token for a CAS authentication (service ticket + user id + remember me).
+ *
+ * @since 1.2
+ */
+public class CasToken implements RememberMeAuthenticationToken {
+    
+    private static final long serialVersionUID = 8587329689973009598L;
+    
+    // the service ticket returned by the CAS server
+    private String ticket = null;
+    
+    // the user identifier
+    private String userId = null;
+    
+    // is the user in a remember me mode ?
+    private boolean isRememberMe = false;
+    
+    public CasToken(String ticket) {
+        this.ticket = ticket;
+    }
+    
+    public Object getPrincipal() {
+        return userId;
+    }
+    
+    public Object getCredentials() {
+        return ticket;
+    }
+    
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+    
+    public boolean isRememberMe() {
+        return isRememberMe;
+    }
+    
+    public void setRememberMe(boolean isRememberMe) {
+        this.isRememberMe = isRememberMe;
+    }
+}

Added: shiro/trunk/support/cas/src/test/groovy/org/apache/shiro/cas/CasRealmTest.groovy
URL: http://svn.apache.org/viewvc/shiro/trunk/support/cas/src/test/groovy/org/apache/shiro/cas/CasRealmTest.groovy?rev=1226983&view=auto
==============================================================================
--- shiro/trunk/support/cas/src/test/groovy/org/apache/shiro/cas/CasRealmTest.groovy (added)
+++ shiro/trunk/support/cas/src/test/groovy/org/apache/shiro/cas/CasRealmTest.groovy Tue Jan  3 23:12:49 2012
@@ -0,0 +1,173 @@
+/*
+ * 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.shiro.cas
+
+import org.apache.shiro.authc.AuthenticationInfo
+import org.apache.shiro.authz.AuthorizationInfo
+
+/**
+ * Unit tests for the {@link CasRealm} implementation.
+ *
+ * @since 1.2
+ */
+class CasRealmTest extends GroovyTestCase {
+
+    /**
+     * Creates a CAS realm with a ticket validator mock.
+     *
+     * @return CasRealm The CAS realm for testing.
+     */
+    private CasRealm createCasRealm() {
+        new CasRealm(ticketValidator: new MockServiceTicketValidator());
+    }
+
+    void testNoAttribute() {
+        CasRealm casRealm = createCasRealm();
+        CasToken casToken = new CasToken('$=defaultId');
+        AuthenticationInfo authenticationInfo = casRealm.doGetAuthenticationInfo(casToken);
+        def principals = authenticationInfo.principals
+        assertEquals "defaultId", principals.primaryPrincipal
+        def attributes = principals.asList()[1] //returns a map
+        assertEquals 0, attributes.size()
+        AuthorizationInfo authorizationInfo = casRealm.doGetAuthorizationInfo(principals);
+        assertNull authorizationInfo.stringPermissions
+        assertNull authorizationInfo.roles
+    }
+
+    void testNoAttributeDefaultRoleAndPermission() {
+        CasRealm casRealm = createCasRealm();
+        casRealm.defaultRoles = "defaultRole"
+        casRealm.defaultPermissions = "defaultPermission"
+        CasToken casToken = new CasToken('$=defaultId');
+        AuthenticationInfo authenticationInfo = casRealm.doGetAuthenticationInfo(casToken);
+        def principals = authenticationInfo.principals
+        assertEquals "defaultId", principals.primaryPrincipal
+        def attributes = principals.oneByType(Map)
+        assertEquals 0, attributes.size()
+        AuthorizationInfo authorizationInfo = casRealm.doGetAuthorizationInfo(principals);
+        assertTrue authorizationInfo.roles.contains("defaultRole")
+        assertTrue authorizationInfo.stringPermissions.contains("defaultPermission")
+    }
+
+    void testNoAttributeDefaultRolesAndPermissions() {
+        CasRealm casRealm = createCasRealm();
+        casRealm.defaultRoles = "defaultRole1, defaultRole2"
+        casRealm.defaultPermissions = "defaultPermission1,defaultPermission2"
+        CasToken casToken = new CasToken('$=defaultId');
+        AuthenticationInfo authcInfo = casRealm.doGetAuthenticationInfo(casToken);
+        def principals = authcInfo.principals
+        assertEquals "defaultId", principals.primaryPrincipal
+        def attributes = principals.oneByType(Map)
+        assertEquals 0, attributes.size()
+        AuthorizationInfo authzInfo = casRealm.doGetAuthorizationInfo(principals)
+        assertEquals 2, authzInfo.roles.size()
+        assertTrue authzInfo.roles.contains("defaultRole1")
+        assertTrue authzInfo.roles.contains("defaultRole2")
+        assertEquals 2, authzInfo.stringPermissions.size()
+        assertTrue authzInfo.stringPermissions.contains("defaultPermission1")
+        assertTrue authzInfo.stringPermissions.contains("defaultPermission2")
+    }
+
+    void testRoleAndPermission() {
+        CasRealm casRealm = createCasRealm();
+        casRealm.roleAttributeNames = "role"
+        casRealm.permissionAttributeNames = "permission"
+        CasToken casToken = new CasToken('$=defaultId|role=aRole|permission=aPermission');
+        AuthenticationInfo authcInfo = casRealm.doGetAuthenticationInfo(casToken);
+        def principals = authcInfo.principals
+        assertEquals "defaultId", principals.primaryPrincipal
+        def attributes = principals.oneByType(Map)
+        assertEquals 2, attributes.size()
+        assertEquals "aRole", attributes['role']
+        assertEquals "aPermission", attributes['permission']
+        AuthorizationInfo authzInfo = casRealm.doGetAuthorizationInfo(principals);
+        assertTrue authzInfo.roles.contains("aRole")
+        assertTrue authzInfo.stringPermissions.contains("aPermission")
+    }
+
+    void testRolesAndPermissions() {
+        CasRealm casRealm = createCasRealm();
+        casRealm.setRoleAttributeNames("role1 , role2");
+        casRealm.setPermissionAttributeNames("permission1,permission2");
+        CasToken casToken = new CasToken(
+                '$=defaultId|role1=role11 , role12|role2=role21,role22|permission1=permission11, permission12|permission2=permission21 ,permission22');
+        AuthenticationInfo authcInfo = casRealm.doGetAuthenticationInfo(casToken);
+        def principals = authcInfo.principals
+        assertEquals "defaultId", principals.primaryPrincipal
+        def attributes = principals.oneByType(Map)
+        assertEquals "role11 , role12", attributes['role1']
+        assertEquals "role21,role22", attributes['role2']
+        assertEquals "permission11, permission12", attributes['permission1']
+        assertEquals "permission21 ,permission22", attributes['permission2']
+        AuthorizationInfo authzInfo = casRealm.doGetAuthorizationInfo(principals);
+        assertEquals 4, authzInfo.roles.size()
+        assertTrue authzInfo.roles.contains("role11")
+        assertTrue authzInfo.roles.contains("role12")
+        assertTrue authzInfo.roles.contains("role21")
+        assertTrue authzInfo.roles.contains("role22")
+        assertTrue authzInfo.stringPermissions.contains("permission11")
+        assertTrue authzInfo.stringPermissions.contains("permission12")
+        assertTrue authzInfo.stringPermissions.contains("permission21")
+        assertTrue authzInfo.stringPermissions.contains("permission22")
+    }
+
+    void testNotRememberMe() {
+        CasRealm casRealm = createCasRealm();
+        CasToken casToken = new CasToken("\$=defaultId|$CasRealm.DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME=false");
+        AuthenticationInfo authcInfo = casRealm.doGetAuthenticationInfo(casToken);
+        def principals = authcInfo.principals
+        assertEquals "defaultId", principals.primaryPrincipal
+        def attributes = principals.oneByType(Map)
+        assertEquals "false", attributes[CasRealm.DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME]
+        assertFalse casToken.rememberMe
+        AuthorizationInfo authzInfo = casRealm.doGetAuthorizationInfo(principals);
+        assertNull authzInfo.stringPermissions
+        assertNull authzInfo.roles
+    }
+
+    void testRememberMe() {
+        CasRealm casRealm = createCasRealm();
+        CasToken casToken = new CasToken("\$=defaultId|$CasRealm.DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME=true");
+        AuthenticationInfo authcInfo = casRealm.doGetAuthenticationInfo(casToken);
+        def principals = authcInfo.principals
+        assertEquals "defaultId", principals.primaryPrincipal
+        def attributes = principals.oneByType(Map)
+        assertEquals "true", attributes[CasRealm.DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME]
+        assertTrue casToken.rememberMe
+        AuthorizationInfo authzInfo = casRealm.doGetAuthorizationInfo(principals);
+        assertNull authzInfo.stringPermissions
+        assertNull authzInfo.roles
+    }
+
+    void testRememberMeNewAttributeName() {
+        CasRealm casRealm = createCasRealm();
+        casRealm.rememberMeAttributeName = "rme"
+        CasToken casToken = new CasToken('$=defaultId|rme=true');
+        AuthenticationInfo authcInfo = casRealm.doGetAuthenticationInfo(casToken);
+        def principals = authcInfo.principals
+        assertEquals "defaultId", principals.primaryPrincipal
+        def attributes = principals.oneByType(Map)
+        assertEquals "true", attributes[casRealm.rememberMeAttributeName]
+        assertTrue casToken.rememberMe
+        AuthorizationInfo authzInfo = casRealm.doGetAuthorizationInfo(principals);
+        assertNull authzInfo.stringPermissions
+        assertNull authzInfo.roles
+    }
+
+}

Added: shiro/trunk/support/cas/src/test/groovy/org/apache/shiro/cas/CasTokenTest.groovy
URL: http://svn.apache.org/viewvc/shiro/trunk/support/cas/src/test/groovy/org/apache/shiro/cas/CasTokenTest.groovy?rev=1226983&view=auto
==============================================================================
--- shiro/trunk/support/cas/src/test/groovy/org/apache/shiro/cas/CasTokenTest.groovy (added)
+++ shiro/trunk/support/cas/src/test/groovy/org/apache/shiro/cas/CasTokenTest.groovy Tue Jan  3 23:12:49 2012
@@ -0,0 +1,46 @@
+/*
+ * 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.shiro.cas
+
+/**
+ * Unit tests for the {@link CasToken} implementation.
+ *
+ * @since 1.2
+ */
+class CasTokenTest extends GroovyTestCase {
+
+    void testPrincipal() {
+        CasToken casToken = new CasToken("fakeTicket")
+        assertNull casToken.principal
+        casToken.userId = "myUserId"
+        assertEquals "myUserId", casToken.principal
+    }
+
+    void testCredentials() {
+        CasToken casToken = new CasToken("fakeTicket")
+        assertEquals "fakeTicket", casToken.credentials
+    }
+
+    void testRememberMe() {
+        CasToken casToken = new CasToken("fakeTicket")
+        assertFalse casToken.rememberMe
+        casToken.rememberMe = true
+        assertTrue casToken.rememberMe
+    }
+}

Added: shiro/trunk/support/cas/src/test/groovy/org/apache/shiro/cas/MockServiceTicketValidator.groovy
URL: http://svn.apache.org/viewvc/shiro/trunk/support/cas/src/test/groovy/org/apache/shiro/cas/MockServiceTicketValidator.groovy?rev=1226983&view=auto
==============================================================================
--- shiro/trunk/support/cas/src/test/groovy/org/apache/shiro/cas/MockServiceTicketValidator.groovy (added)
+++ shiro/trunk/support/cas/src/test/groovy/org/apache/shiro/cas/MockServiceTicketValidator.groovy Tue Jan  3 23:12:49 2012
@@ -0,0 +1,57 @@
+/*
+ * 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.shiro.cas
+
+import org.apache.shiro.util.StringUtils
+import org.jasig.cas.client.authentication.AttributePrincipalImpl
+import org.jasig.cas.client.validation.Assertion
+import org.jasig.cas.client.validation.AssertionImpl
+import org.jasig.cas.client.validation.TicketValidationException
+import org.jasig.cas.client.validation.TicketValidator
+
+/**
+ * @since 1.2
+ */
+class MockServiceTicketValidator implements TicketValidator {
+
+    /**
+     * Returns different assertions according to the ticket input. The format of the mock ticket must be :
+     * key1=value1,key2=value2,...,keyN=valueN. If keyX is $, valueX is considered to be the name of the principal, otherwise (keyX, valueX)
+     * is considered to be an attribute of the principal.
+     */
+    public Assertion validate(String ticket, String service) throws TicketValidationException {
+        String name = null;
+        def attributes = [:]
+        String[] elements = StringUtils.split(ticket, '|' as char);
+        int length = elements.length;
+        for (int i = 0; i < length; i++) {
+            String[] pair = StringUtils.split(elements[i], '=' as char);
+            String key = pair[0].trim();
+            String value = pair[1].trim();
+            if ('$'.equals(key)) {
+                name = value;
+            } else {
+                attributes.put(key, value);
+            }
+        }
+        AttributePrincipalImpl attributePrincipalImpl = new AttributePrincipalImpl(name, attributes);
+        return new AssertionImpl(attributePrincipalImpl, [:]);
+
+    }
+}

Modified: shiro/trunk/support/pom.xml
URL: http://svn.apache.org/viewvc/shiro/trunk/support/pom.xml?rev=1226983&r1=1226982&r2=1226983&view=diff
==============================================================================
--- shiro/trunk/support/pom.xml (original)
+++ shiro/trunk/support/pom.xml Tue Jan  3 23:12:49 2012
@@ -39,6 +39,7 @@
         <module>guice</module>
         <module>openid4j</module>
         <module>features</module>
+        <module>cas</module>
     </modules>
 
 </project>



Mime
View raw message