net.shibboleth.idp.session.AbstractIdPSession.java Source code

Java tutorial

Introduction

Here is the source code for net.shibboleth.idp.session.AbstractIdPSession.java

Source

/*
 * Licensed to the University Corporation for Advanced Internet Development, 
 * Inc. (UCAID) under one or more contributor license agreements.  See the 
 * NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The UCAID 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 net.shibboleth.idp.session;

import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;

import net.shibboleth.idp.authn.AuthenticationResult;
import net.shibboleth.utilities.java.support.annotation.Duration;
import net.shibboleth.utilities.java.support.annotation.constraint.Live;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
import net.shibboleth.utilities.java.support.logic.Constraint;
import net.shibboleth.utilities.java.support.primitive.StringSupport;

import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.MoreObjects;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;

/**
 * Abstract base for implementations of {@link IdPSession}, handles basic management of the
 * instance data without addressing persistence.
 * 
 * <p>Data that can change post-construction can be modified using doSet/doAdd/doRemove methods
 * that maintain the object state. Abstract methods defined here or left unimplemented from the
 * interface should be implemented to call these methods and perform any additional work required
 * to maintain the coherence of the underlying store, if any.</p>
 * 
 * <p>The {@link #checkAddress(String)} method is implemented by calling into other abstract and defined
 * methods to check session state and update address information as required.</p> 
 */
@ThreadSafe
public abstract class AbstractIdPSession implements IdPSession {

    /** Address syntaxes supported for address binding. */
    public enum AddressFamily {
        /** IP version 4 (dotted decimal). */
        IPV4,

        /** IP version 6 (colon-sep hex). */
        IPV6,

        /** Unknown. */
        UNKNOWN
    }

    /** Class logger. */
    @Nonnull
    private final Logger log = LoggerFactory.getLogger(AbstractIdPSession.class);

    /** Unique ID of this session. */
    @Nonnull
    @NotEmpty
    private final String id;

    /** A canonical name for the subject of the session. */
    @Nonnull
    @NotEmpty
    private final String principalName;

    /** Time, in milliseconds since the epoch, when this session was created. */
    @Duration
    private final long creationInstant;

    /** Last activity instant, in milliseconds since the epoch, for this session. */
    @Duration
    private long lastActivityInstant;

    /** Addresses to which the session is bound. */
    @Nullable
    private String ipV4Address;

    /** An IPv6 address to which the session is bound. */
    @Nullable
    private String ipV6Address;

    /** Tracks authentication results that have occurred during this session. */
    @Nonnull
    private final ConcurrentMap<String, Optional<AuthenticationResult>> authenticationResults;

    /** Tracks services which have been issued authentication tokens during this session. */
    @Nonnull
    private final ConcurrentMap<String, Optional<SPSession>> spSessions;

    /**
     * Constructor.
     * 
     * @param sessionId identifier for this session
     * @param canonicalName canonical name of subject
     * @param creationTime creation time of session in milliseconds
     */
    public AbstractIdPSession(@Nonnull @NotEmpty final String sessionId,
            @Nonnull @NotEmpty final String canonicalName, @Positive final long creationTime) {
        id = Constraint.isNotNull(StringSupport.trimOrNull(sessionId), "Session ID cannot be null or empty");
        principalName = Constraint.isNotNull(StringSupport.trimOrNull(canonicalName),
                "Principal name cannot be null or empty.");

        creationInstant = Constraint.isGreaterThan(0, creationTime, "Creation time must be greater than 0");
        lastActivityInstant = creationTime;

        authenticationResults = new ConcurrentHashMap(5);
        spSessions = new ConcurrentHashMap(10);
    }

    /** {@inheritDoc} */
    @Override
    @Nonnull
    @NotEmpty
    public String getId() {
        return id;
    }

    /** {@inheritDoc} */
    @Override
    @Nonnull
    @NotEmpty
    public String getPrincipalName() {
        return principalName;
    }

    /** {@inheritDoc} */
    @Override
    public long getCreationInstant() {
        return creationInstant;
    }

    /** {@inheritDoc} */
    @Override
    public long getLastActivityInstant() {
        return lastActivityInstant;
    }

    /**
     * Set the last activity instant, in milliseconds since the epoch, for the session.
     * 
     * @param instant last activity instant, in milliseconds since the epoch, for the session, must be greater than 0
     * @throws SessionException if an error occurs updating the session
     */
    public void setLastActivityInstant(@Duration @Positive final long instant) throws SessionException {
        doSetLastActivityInstant(instant);
    }

    /**
     * Set the last activity instant, in milliseconds since the epoch, for the session.
     * 
     * <p>This manipulates only the internal state of the object. The {@link #setLastActivityInstant(long)}
     * method must be overridden to support other persistence requirements.</p>
     * 
     * @param instant last activity instant, in milliseconds since the epoch, for the session, must be greater than 0
     */
    public void doSetLastActivityInstant(@Duration @Positive final long instant) {
        lastActivityInstant = Constraint.isGreaterThan(0, instant, "Last activity instant must be greater than 0");
    }

    /**
     * Get an address to which this session is bound.
     * 
     * @param family the address family to inquire
     * 
     * @return bound address or null
     */
    @Nullable
    public String getAddress(@Nonnull final AddressFamily family) {
        switch (family) {
        case IPV4:
            return ipV4Address;
        case IPV6:
            return ipV6Address;
        default:
            return null;
        }
    }

    /**
     * Associate an address with this session.
     * 
     * @param address the address to associate
     * @throws SessionException if an error occurs binding the address to the session
     */
    public void bindToAddress(@Nonnull @NotEmpty final String address) throws SessionException {
        doBindToAddress(address);
    }

    /**
     * Associate an address with this session.
     * 
     * <p>This manipulates only the internal state of the object. The {@link #bindToAddress(String)}
     * method must be overridden to support other persistence requirements.</p>
     * 
     * @param address the address to associate
     */
    public void doBindToAddress(@Nonnull @NotEmpty final String address) {
        String trimmed = Constraint.isNotNull(StringSupport.trimOrNull(address), "Address cannot be null or empty");
        switch (getAddressFamily(address)) {
        case IPV6:
            ipV6Address = StringSupport.trimOrNull(trimmed);
            break;

        case IPV4:
            ipV4Address = StringSupport.trimOrNull(trimmed);
            break;

        default:
            log.warn("Unsupported address form {}", address);
        }
    }

    /** {@inheritDoc} */
    @Override
    @Nonnull
    @NonnullElements
    @NotLive
    @Unmodifiable
    public Set<AuthenticationResult> getAuthenticationResults() {
        return ImmutableSet.copyOf(Optional.presentInstances(authenticationResults.values()));
    }

    /** {@inheritDoc} */
    @Override
    @Nullable
    public AuthenticationResult getAuthenticationResult(@Nonnull @NotEmpty final String flowId) {
        Optional<AuthenticationResult> mapped = authenticationResults.get(StringSupport.trimOrNull(flowId));
        return (mapped != null) ? mapped.orNull() : null;
    }

    /** {@inheritDoc} */
    @Override
    @Nullable
    public AuthenticationResult addAuthenticationResult(@Nonnull final AuthenticationResult result)
            throws SessionException {
        return doAddAuthenticationResult(result);
    }

    /** {@inheritDoc} */
    @Override
    public boolean removeAuthenticationResult(@Nonnull final AuthenticationResult result) throws SessionException {
        return doRemoveAuthenticationResult(result);
    }

    /**
     * Add a new {@link AuthenticationResult} to this IdP session, replacing any
     * existing result of the same flow ID.
     * 
     * <p>This manipulates only the internal state of the object. The
     * {@link #addAuthenticationResult(AuthenticationResult)} method must be implemented to support
     * other persistence requirements.</p>
     * 
     * @param result the result to add
     * 
     * @return a previously existing result replaced by the new one, if any
     */
    @Nullable
    public AuthenticationResult doAddAuthenticationResult(@Nonnull final AuthenticationResult result) {
        Constraint.isNotNull(result, "AuthenticationResult cannot be null");

        Optional<AuthenticationResult> prev = authenticationResults.put(result.getAuthenticationFlowId(),
                Optional.of(result));
        if (prev != null && prev.isPresent()) {
            log.debug("IdPSession {}: replaced old AuthenticationResult for flow ID {}", id,
                    prev.get().getAuthenticationFlowId());
            return prev.get();
        }
        return null;
    }

    /**
     * Disassociate an {@link AuthenticationResult} from this IdP session.
     * 
     * <p>This manipulates only the internal state of the object. The
     * {@link #removeAuthenticationResult(AuthenticationResult)} method must be implemented to support
     * other persistence requirements.</p>
     * 
     * @param result the result to disassociate
     * 
     * @return true iff the given result had been associated with this IdP session and now is not
     */
    public boolean doRemoveAuthenticationResult(@Nonnull final AuthenticationResult result) {
        Constraint.isNotNull(result, "Authentication event can not be null");

        // Record may be actually present, or not yet loaded.
        if (authenticationResults.remove(result.getAuthenticationFlowId(), Optional.of(result))) {
            return true;
        } else {
            return authenticationResults.remove(result.getAuthenticationFlowId(), Optional.absent());
        }
    }

    /** {@inheritDoc} */
    @Override
    @Nonnull
    @NonnullElements
    @NotLive
    @Unmodifiable
    public Set<SPSession> getSPSessions() {
        return ImmutableSet.copyOf(Optional.presentInstances(spSessions.values()));
    }

    /** {@inheritDoc} */
    @Override
    @Nullable
    public SPSession getSPSession(@Nonnull @NotEmpty final String serviceId) {
        Optional<SPSession> mapped = spSessions.get(StringSupport.trimOrNull(serviceId));
        return (mapped != null) ? mapped.orNull() : null;
    }

    /** {@inheritDoc} */
    @Override
    @Nullable
    public SPSession addSPSession(@Nonnull final SPSession spSession) throws SessionException {
        return doAddSPSession(spSession);
    }

    /** {@inheritDoc} */
    @Override
    public boolean removeSPSession(@Nonnull final SPSession spSession) throws SessionException {
        return doRemoveSPSession(spSession);
    }

    /**
     * Add a new SP session to this IdP session, replacing any existing session for the same
     * service.
     * 
     * <p>This manipulates only the internal state of the object. The {@link #addSPSession(SPSession)}
     * method must be implemented to support other persistence requirements.</p>
     * 
     * @param spSession the SP session
     * 
     * @return a previously existing SPSession replaced by the new one, if any
     */
    @Nullable
    public SPSession doAddSPSession(@Nonnull final SPSession spSession) {
        Constraint.isNotNull(spSession, "SPSession cannot be null");

        Optional<SPSession> prev = spSessions.put(spSession.getId(), Optional.of(spSession));
        if (prev != null && prev.isPresent()) {
            log.debug("IdPSession {}: replaced old SPSession for service {}", id, prev.get().getId());
            return prev.get();
        }
        return null;
    }

    /**
     * Disassociate the given SP session from this IdP session.
     * 
     * <p>This manipulates only the internal state of the object. The {@link #removeSPSession(SPSession)}
     * method must be implemented to support other persistence requirements.</p>
     * 
     * @param spSession the SP session
     * 
     * @return true iff the given SP session had been associated with this IdP session and now is not
     */
    public boolean doRemoveSPSession(@Nonnull final SPSession spSession) {
        Constraint.isNotNull(spSession, "SPSession cannot be null");

        // Record may be actually present, or not yet loaded.
        if (spSessions.remove(spSession.getId(), Optional.of(spSession))) {
            return true;
        } else {
            return spSessions.remove(spSession.getId(), Optional.absent());
        }
    }

    /** {@inheritDoc} */
    @Override
    public boolean checkAddress(@Nonnull @NotEmpty final String address) throws SessionException {
        AddressFamily family = getAddressFamily(address);
        if (family == AddressFamily.UNKNOWN) {
            log.warn("Address {} is of unknown type", address);
            return false;
        }
        String bound = getAddress(family);
        if (bound != null) {
            if (!bound.equals(address)) {
                log.warn("Client address is {} but session {} already bound to {}", address, id, bound);
                return false;
            }
        } else {
            log.info("Session {} not yet locked to a {} address, locking it to {}", id, family, address);
            try {
                bindToAddress(address);
            } catch (SessionException e) {
                log.error("Unable to bind session {} to address {}", id, address);
                return false;
            }
        }

        return true;
    }

    /** {@inheritDoc} */
    @Override
    public boolean checkTimeout() throws SessionException {
        setLastActivityInstant(System.currentTimeMillis());
        return true;
    }

    /** {@inheritDoc} */
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }

        if (this == obj) {
            return true;
        }

        if (obj instanceof AbstractIdPSession) {
            return Objects.equals(getId(), ((AbstractIdPSession) obj).getId());
        }

        return false;
    }

    /** {@inheritDoc} */
    @Override
    public int hashCode() {
        return id.hashCode();
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this).add("sessionId", id).add("principalName", principalName)
                .add("IPv4", ipV4Address).add("IPv6", ipV6Address)
                .add("creationInstant", new DateTime(creationInstant))
                .add("lastActivityInstant", new DateTime(lastActivityInstant))
                .add("authenticationResults", getAuthenticationResults()).add("spSessions", getSPSessions())
                .toString();
    }

    /**
     * Accessor for the underlying {@link AuthenticationResult} map maintained with the IdP session.
     * 
     * @return direct access to the result map
     */
    @Nonnull
    @NonnullElements
    @Live
    protected Map<String, Optional<AuthenticationResult>> getAuthenticationResultMap() {
        return authenticationResults;
    }

    /**
     * Accessor for the underlying {@link SPSession} map maintained with the IdP session.
     * 
     * @return direct access to the service session map
     */
    @Nonnull
    @NonnullElements
    @Live
    protected Map<String, Optional<SPSession>> getSPSessionMap() {
        return spSessions;
    }

    /**
     * Returns the address family for an input address.
     * 
     * @param address   the string to check
     * @return the address family
     */
    @Nonnull
    protected static AddressFamily getAddressFamily(@Nonnull @NotEmpty final String address) {
        if (address.contains(":")) {
            return AddressFamily.IPV6;
        } else if (address.contains(".")) {
            return AddressFamily.IPV4;
        } else {
            return AddressFamily.UNKNOWN;
        }
    }

}