Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated Syncer to use new session-data-redis version. #229

Merged
merged 1 commit into from
May 20, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gate-web/gate-web.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dependencies {
compile spinnaker.dependency("korkWeb")
compile spinnaker.dependency("frigga")
compile spinnaker.dependency('cglib')
compile('com.github.kstyrc:embedded-redis:0.6')
compile('org.springframework.session:spring-session-data-redis:1.1.1.RELEASE')
compile("org.springframework.cloud:spring-cloud-security:1.0.3.RELEASE")
compile('org.opensaml:opensaml:2.6.4')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@ import org.springframework.data.redis.core.RedisTemplate
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.security.core.context.SecurityContext
import org.springframework.session.ExpiringSession
import org.springframework.session.Session
import org.springframework.session.SessionRepository

@Slf4j
@Configuration
class UserRolesSyncer {
@Autowired
RedisTemplate sessionRedisTemplate

@Autowired
RedisTemplate<String, ExpiringSession> redisTemplate
SessionRepository<? extends ExpiringSession> repository

@Autowired
UserRolesProvider userRolesProvider
Expand All @@ -41,30 +45,40 @@ class UserRolesSyncer {
*/
@Scheduled(initialDelay = 10000L, fixedRate = 600000L)
public void syncUserGroups() {
Map<String, String> emailSessionKeyMap = [:]
Map<String, String> emailSessionIdMap = [:]
Map<String, Collection<String>> emailCurrentGroupsMap = [:]
Set<String> sessionKeys = redisTemplate.keys('*session:sessions*')
Set<String> sessionKeys = sessionRedisTemplate.keys('*session:sessions*')

Set<String> sessionIds = sessionKeys.collect { String key ->
def toks = key.split(":")
toks[toks.length - 1]
}

sessionKeys.each { String key ->
def secCtx = redisTemplate.opsForHash().get(key, "sessionAttr:SPRING_SECURITY_CONTEXT") as SecurityContext
def principal = secCtx?.authentication?.principal
if (principal && principal instanceof User) {
emailSessionKeyMap[principal.email] = key
emailCurrentGroupsMap[principal.email] = principal.roles
sessionIds.each { String id ->
Session session = repository.getSession(id)
if (session) { // getSession(id) may return null if session is expired but not reaped
def secCtx = session.getAttribute("SPRING_SECURITY_CONTEXT") as SecurityContext
def principal = secCtx?.authentication?.principal
if (principal && principal instanceof User) {
emailSessionIdMap[principal.email] = id
emailCurrentGroupsMap[principal.email] = principal.roles
}
}
}

def newGroupsMap = userRolesProvider.multiLoadRoles(emailSessionKeyMap.keySet())
def sessionKeysToDelete = []
def newGroupsMap = userRolesProvider.multiLoadRoles(emailSessionIdMap.keySet())
def sessionIdsToDelete = []
newGroupsMap.each { String email, Collection<String> newGroups ->
// cast for equals check to work
List<String> newList = newGroups as List
List<String> oldList = emailCurrentGroupsMap[email] as List
if (oldList != newList) {
sessionKeysToDelete.add(emailSessionKeyMap[email])
sessionIdsToDelete.add(emailSessionIdMap[email])
}
}
redisTemplate.delete(sessionKeysToDelete)
log.info("Invalidated {} user sessions due to changed group memberships.", sessionKeysToDelete.size())

def keysToDelete = sessionIdsToDelete.collect { String id -> "spring:session:sessions:" + id }
sessionRedisTemplate.delete(keysToDelete)
log.info("Invalidated {} user sessions due to changed group memberships.", keysToDelete.size())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2016 Google, Inc.
*
* Licensed 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 com.netflix.spinnaker.gate.redis

import redis.clients.jedis.Jedis
import redis.clients.jedis.JedisPool
import redis.clients.util.Pool
import redis.embedded.RedisServer

public class EmbeddedRedis {

private final URI connection;
private final RedisServer redisServer;

private Pool<Jedis> jedis;

private EmbeddedRedis(int port) throws IOException, URISyntaxException {
this.connection = URI.create(String.format("redis://127.0.0.1:%d/0", port));
this.redisServer = RedisServer
.builder()
.port(port)
.setting("bind 127.0.0.1")
.setting("appendonly no")
.setting("save \"\"")
.setting("databases 1")
.build();
this.redisServer.start();
}

public void destroy() {
try {
this.redisServer.stop();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public int getPort() {
return redisServer.ports().get(0);
}

public Pool<Jedis> getPool() {
if (jedis == null) {
jedis = new JedisPool(connection);
}
return jedis;
}

public Jedis getJedis() {
return getPool().getResource();
}

public static EmbeddedRedis embed() {
try {
ServerSocket serverSocket = new ServerSocket(0);
int port = serverSocket.getLocalPort();
serverSocket.close();
return new EmbeddedRedis(port);
} catch (IOException | URISyntaxException e) {
throw new RuntimeException("Failed to create embedded Redis", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/*
* Copyright 2016 Google, Inc.
*
* Licensed 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 com.netflix.spinnaker.gate.security.rolesprovider

import com.netflix.spinnaker.gate.redis.EmbeddedRedis
import com.netflix.spinnaker.security.User
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.security.core.context.SecurityContextImpl
import org.springframework.security.oauth2.provider.OAuth2Authentication
import org.springframework.security.oauth2.provider.OAuth2Request
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
import org.springframework.session.data.redis.RedisOperationsSessionRepository
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.web.WebAppConfiguration
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Subject

@WebAppConfiguration
@ContextConfiguration
class UserRolesSyncerSpec extends Specification {

/**
* Port the EmbeddedRedis instance associated with this test is listening on.
*/
static Integer port

@Shared
@AutoCleanup("destroy")
EmbeddedRedis embeddedRedis

@Autowired
RedisTemplate sessionRedisTemplate

@Autowired
RedisOperationsSessionRepository repository

@Shared
@Subject
UserRolesSyncer userRolesSyncer

def setupSpec() {
embeddedRedis = EmbeddedRedis.embed()
embeddedRedis.jedis.flushDB()
port = embeddedRedis.port
}

def cleanup() {
embeddedRedis.jedis.flushDB()
}

def createSpringSession(String email, List<String> roles) {
def session = repository.createSession()

User spinnakerUser = new User(
email: email,
firstName: "User",
lastName: "McUserFace",
allowedAccounts: ["AnonAllowedAccount"],
roles: roles,
)

PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(
spinnakerUser,
null /* credentials */,
spinnakerUser.authorities
)

// impl copied from userInfoTokenServices
OAuth2Request storedRequest = new OAuth2Request(null, "fake_test_client_id", null, true /*approved*/,
null, null, null, null, null);

def oauthentication = new OAuth2Authentication(storedRequest, authentication)
def secCtx = new SecurityContextImpl()
secCtx.setAuthentication(oauthentication)
session.setAttribute("SPRING_SECURITY_CONTEXT", secCtx)

repository.save(session)
return session.getId()
}

def "should invalidate the session"() {
given: "the session's groups are not up to date"
userRolesSyncer = new UserRolesSyncer(sessionRedisTemplate: sessionRedisTemplate, repository: repository)
def sessionId = createSpringSession("user@mcuserface.com", ["noob"])
def rolesProvider = new UserRolesProvider() {
@Override
Collection<String> loadRoles(String userEmail) {
return []
}

@Override
Map<String, Collection<String>> multiLoadRoles(Collection<String> userEmails) {
return ["user@mcuserface.com": ["real_user"]]
}
}
userRolesSyncer.userRolesProvider = rolesProvider

when: "we sync the groups"
userRolesSyncer.syncUserGroups()

then: "no sessions should be present"
repository.getSession(sessionId) == null
}

def "should not invalidate the session"() {
given: "the session's groups are up to date"
userRolesSyncer = new UserRolesSyncer(sessionRedisTemplate: sessionRedisTemplate, repository: repository)
def sessionId = createSpringSession("user1@mcuserface.com", ["noob"])
def rolesProvider = new UserRolesProvider() {
@Override
Collection<String> loadRoles(String userEmail) {
return []
}

@Override
Map<String, Collection<String>> multiLoadRoles(Collection<String> userEmails) {
return ["user1@mcuserface.com": ["noob"]]
}
}
userRolesSyncer.userRolesProvider = rolesProvider

when: "we sync the groups"
userRolesSyncer.syncUserGroups()

then: "our session should be present"
repository.getSession(sessionId) != null
}

def "should invalidate only bad sessions"() {
given: "the session's groups are up to date"
userRolesSyncer = new UserRolesSyncer(sessionRedisTemplate: sessionRedisTemplate, repository: repository)
def goodSessionId = createSpringSession("user2@mcuserface.com", ["noob", "real_user"])
def badSessionId = createSpringSession("admin@mcuserface.com", ["real_user"])
def rolesProvider = new UserRolesProvider() {
@Override
Collection<String> loadRoles(String userEmail) {
return []
}

@Override
Map<String, Collection<String>> multiLoadRoles(Collection<String> userEmails) {
return [
"user2@mcuserface.com": ["noob", "real_user"],
"admin@mcuserface.com": [],
]
}
}
userRolesSyncer.userRolesProvider = rolesProvider

when: "we sync the groups"
userRolesSyncer.syncUserGroups()

then: "our session should be present"
repository.getSession(goodSessionId) != null
repository.getSession(badSessionId) == null
}

@Configuration
@EnableRedisHttpSession
static class Config {
@Bean
public JedisConnectionFactory connectionFactory() {
URI redis = URI.create("redis://localhost:" + port.toString())
def factory = new JedisConnectionFactory()
factory.hostName = redis.host
factory.port = redis.port
if (redis.userInfo) {
factory.password = redis.userInfo.split(":", 2)[1]
}
factory
}
}
}