package gov.nysenate.openleg.service.auth; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import gov.nysenate.openleg.model.auth.ApiKeyLoginToken; import gov.nysenate.openleg.model.auth.ApiUserAuthEvictEvent; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authc.credential.CredentialsMatcher; import org.apache.shiro.authc.pam.UnsupportedTokenException; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.cache.Cache; import org.apache.shiro.subject.PrincipalCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @Component public class ApiUserLoginAuthRealm extends OpenLegAuthorizingRealm { private static final Logger logger = LoggerFactory.getLogger(ApiUserLoginAuthRealm.class); private static final class ApiCredentialsMatcher implements CredentialsMatcher { /** The credentials will always match if the auth token and info have the same principal.*/ @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { if (token != null && info != null && token.getPrincipal() != null && info.getPrincipals() != null) { return StringUtils.equals(token.getPrincipal().toString(), info.getPrincipals().getPrimaryPrincipal().toString()); } return false; } } private static final CredentialsMatcher apiCredentialsMatcher = new ApiCredentialsMatcher(); @Autowired private ApiUserService apiUserService; @Autowired private EventBus eventBus; @Override public void onInit() { super.onInit(); eventBus.register(this); } /** * Check to see if the API Key exists. If it does return the AuthenticationInfo. * * @param token The given authentication information * @return Either valid AuthenticationInfo for the given token or null if the account is not valid * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { if (token != null && token instanceof ApiKeyLoginToken) { ApiKeyLoginToken apiToken = (ApiKeyLoginToken) token; logger.info("Attempting login with API Realm from {} with key {}", apiToken.getHost(), apiToken.getApiKey()); return queryForAuthenticationInfo(apiToken); } throw new UnsupportedTokenException("OpenLeg 2.0 only supports UsernamePasswordToken"); } /** A * If the API Key is valid, return an AuthenticationInfo with the principal as the API key and the * credentials set to null. If the key is not valid, null will be returned. * * @param info The given UsernamePasswordToke * @return A new SimpleAuthenticationInfo object */ protected AuthenticationInfo queryForAuthenticationInfo(ApiKeyLoginToken info) { if (apiUserService.validateKey(info.getApiKey())) { return new SimpleAuthenticationInfo(info.getApiKey(), info.getApiKey(), this.getName()); } return null; } /** * This method assigns the API User role. * * @param principals The identifying attributes of the currently active user * @return A SimpleAuthorizationInfo object containing the roles and permissions of the user. */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { Collection principalCollection = principals.fromRealm(getName()); if (!principalCollection.isEmpty()) { SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo(); String apiKey = principalCollection.iterator().next().toString(); logger.info("Assigning API_USER role to {}", apiKey); authInfo.addRole(OpenLegRole.API_USER.name()); // Add any explicitly defined roles apiUserService.getRoles(apiKey).stream() .map(OpenLegRole::name) .peek(role -> logger.info("Assigning role {} to api user: {}", role, apiKey)) .forEach(authInfo::addRole); return authInfo; } return null; } @Subscribe public void handleApiUserAuthEvictEvent(ApiUserAuthEvictEvent e) { this.clearAuthForKey(e.getApiKey()); } protected void clearAuthForKey(String apiKey) { Cache<Object, AuthorizationInfo> authCache = getAuthorizationCache(); List<Object> keyList = authCache.keys().stream() .filter(principals -> StringUtils.equals(Objects.toString(principals), apiKey)) .collect(Collectors.toList()); keyList.forEach(authCache::remove); } @Override public Class getAuthenticationTokenClass() { return ApiKeyLoginToken.class; } @Override public CredentialsMatcher getCredentialsMatcher() { return apiCredentialsMatcher; } }