/******************************************************************************* * Cloud Foundry * Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved. * * This product is licensed to you under the Apache License, Version 2.0 (the "License"). * You may not use this product except in compliance with the License. * * This product includes a number of subcomponents with * separate copyright notices and license terms. Your use of these * subcomponents is subject to the terms and conditions of the * subcomponent's license, as noted in the LICENSE file. *******************************************************************************/ package org.cloudfoundry.identity.uaa.oauth.approval; import com.fasterxml.jackson.core.type.TypeReference; import org.cloudfoundry.identity.uaa.approval.Approval; import org.cloudfoundry.identity.uaa.approval.Approval.ApprovalStatus; import org.cloudfoundry.identity.uaa.approval.ApprovalsAdminEndpoints; import org.cloudfoundry.identity.uaa.approval.JdbcApprovalStore; import org.cloudfoundry.identity.uaa.error.UaaException; import org.cloudfoundry.identity.uaa.security.SecurityContextAccessor; import org.cloudfoundry.identity.uaa.test.JdbcTestBase; import org.cloudfoundry.identity.uaa.test.TestUtils; import org.cloudfoundry.identity.uaa.test.UaaTestAccounts; import org.cloudfoundry.identity.uaa.user.JdbcUaaUserDatabase; import org.cloudfoundry.identity.uaa.user.UaaUser; import org.cloudfoundry.identity.uaa.user.UaaUserDatabase; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.util.TimeServiceImpl; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.oauth2.provider.NoSuchClientException; import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.security.oauth2.provider.client.InMemoryClientDetailsService; import java.lang.reflect.Method; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Timestamp; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Random; import java.util.Set; import java.util.stream.Collectors; import static org.cloudfoundry.identity.uaa.approval.Approval.ApprovalStatus.APPROVED; import static org.cloudfoundry.identity.uaa.approval.Approval.ApprovalStatus.DENIED; import static org.cloudfoundry.identity.uaa.test.UaaTestAccounts.INSERT_BARE_BONE_USER; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class ApprovalsAdminEndpointsTests extends JdbcTestBase { @Rule public ExpectedException exception = ExpectedException.none(); private UaaTestAccounts testAccounts = null; private JdbcApprovalStore dao; private UaaUserDatabase userDao = null; private UaaUser marissa; private ApprovalsAdminEndpoints endpoints; private Random random = new Random(System.currentTimeMillis()); @Before public void initApprovalsAdminEndpointsTests() { testAccounts = UaaTestAccounts.standard(null); String userId = testAccounts.addRandomUser(jdbcTemplate); userDao = new JdbcUaaUserDatabase(jdbcTemplate, new TimeServiceImpl()); jdbcTemplate = new JdbcTemplate(dataSource); marissa = userDao.retrieveUserById(userId); assertNotNull(marissa); dao = new JdbcApprovalStore(jdbcTemplate); endpoints = new ApprovalsAdminEndpoints(); endpoints.setApprovalStore(dao); endpoints.setUaaUserDatabase(userDao); InMemoryClientDetailsService clientDetailsService = new InMemoryClientDetailsService(); BaseClientDetails details = new BaseClientDetails("c1", "scim,clients", "read,write", "authorization_code, password, implicit, client_credentials", "update"); details.setAutoApproveScopes(Arrays.asList("true")); clientDetailsService.setClientDetailsStore(Collections .singletonMap("c1", details)); endpoints.setClientDetailsService(clientDetailsService); endpoints.setSecurityContextAccessor(mockSecurityContextAccessor(marissa.getUsername(), marissa.getId())); } private void addApproval(String userName, String clientId, String scope, int expiresIn, ApprovalStatus status) { dao.addApproval(new Approval() .setUserId(userName) .setClientId(clientId) .setScope(scope) .setExpiresAt(Approval.timeFromNow(expiresIn)) .setStatus(status)); } private SecurityContextAccessor mockSecurityContextAccessor(String userName, String id) { SecurityContextAccessor sca = mock(SecurityContextAccessor.class); when(sca.getUserName()).thenReturn(userName); when(sca.getUserId()).thenReturn(id); when(sca.isUser()).thenReturn(true); return sca; } @After public void cleanupDataSource() throws Exception { TestUtils.deleteFrom(dataSource, "authz_approvals"); TestUtils.deleteFrom(dataSource, "users"); assertThat(jdbcTemplate.queryForObject("select count(*) from authz_approvals", Integer.class), is(0)); assertThat(jdbcTemplate.queryForObject("select count(*) from users", Integer.class), is(0)); } @Test public void validate_client_id_on_revoke() throws Exception { exception.expect(NoSuchClientException.class); exception.expectMessage("No client with requested id: invalid_id"); endpoints.revokeApprovals("invalid_id"); } @Test public void validate_client_id_on_update() throws Exception { exception.expect(NoSuchClientException.class); exception.expectMessage("No client with requested id: invalid_id"); endpoints.updateClientApprovals("invalid_id", new Approval[0]); } @Test public void canGetApprovals() { addApproval(marissa.getId(), "c1", "uaa.user", 6000, APPROVED); addApproval(marissa.getId(), "c1", "uaa.admin", 12000, DENIED); addApproval(marissa.getId(), "c1", "openid", 6000, APPROVED); assertEquals(3, endpoints.getApprovals("user_id pr", 1, 100).size()); assertEquals(2, endpoints.getApprovals("user_id pr", 1, 2).size()); } @Test public void testApprovalsDeserializationIsCaseInsensitive() throws Exception { Set<Approval> approvals = new HashSet<>(); approvals.add(new Approval() .setUserId("test-user-id") .setClientId("testclientid") .setScope("scope") .setExpiresAt(new Date()) .setStatus(ApprovalStatus.APPROVED)); Set<Approval> deserializedApprovals = JsonUtils.readValue("[{\"userid\":\"test-user-id\",\"clientid\":\"testclientid\",\"scope\":\"scope\",\"status\":\"APPROVED\",\"expiresat\":\"2015-08-25T14:35:42.512Z\",\"lastupdatedat\":\"2015-08-25T14:35:42.512Z\"}]", new TypeReference<Set<Approval>>() { }); assertEquals(approvals, deserializedApprovals); } @Test public void canGetApprovalsWithAutoApproveTrue() { // Only get scopes that need approval addApproval(marissa.getId(), "c1", "uaa.user", 6000, APPROVED); addApproval(marissa.getId(), "c1", "uaa.admin", 12000, DENIED); addApproval(marissa.getId(), "c1", "openid", 6000, APPROVED); assertEquals(3, endpoints.getApprovals("user_id eq \""+marissa.getId()+"\"", 1, 100).size()); addApproval(marissa.getId(), "c1", "read", 12000, DENIED); addApproval(marissa.getId(), "c1", "write", 6000, APPROVED); assertEquals(3, endpoints.getApprovals("user_id eq \""+marissa.getId()+"\"", 1, 100).size()); } @Test public void canUpdateApprovals() { addApproval(marissa.getId(), "c1", "uaa.user", 6000, APPROVED); addApproval(marissa.getId(), "c1", "uaa.admin", 12000, DENIED); addApproval(marissa.getId(), "c1", "openid", 6000, APPROVED); Approval[] app = new Approval[] {new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("uaa.user") .setExpiresAt(Approval.timeFromNow(2000)) .setStatus(APPROVED), new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("dash.user") .setExpiresAt(Approval.timeFromNow(2000)) .setStatus(APPROVED), new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("openid") .setExpiresAt(Approval.timeFromNow(2000)) .setStatus(DENIED), new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("cloud_controller.read") .setExpiresAt(Approval.timeFromNow(2000)) .setStatus(APPROVED)}; List<Approval> response = endpoints.updateApprovals(app); assertEquals(4, response.size()); assertTrue(response.contains(new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("uaa.user") .setExpiresAt(Approval.timeFromNow(2000)) .setStatus(APPROVED))); assertTrue(response.contains(new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("dash.user") .setExpiresAt(Approval.timeFromNow(2000)) .setStatus(APPROVED))); assertTrue(response.contains(new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("openid") .setExpiresAt(Approval.timeFromNow(2000)) .setStatus(DENIED))); assertTrue(response.contains(new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("cloud_controller.read") .setExpiresAt(Approval.timeFromNow(2000)) .setStatus(APPROVED))); List<Approval> updatedApprovals = endpoints.getApprovals("user_id eq \""+marissa.getId()+"\"", 1, 100); assertEquals(4, updatedApprovals.size()); assertTrue(updatedApprovals.contains(new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("dash.user") .setExpiresAt(Approval.timeFromNow(2000)) .setStatus(APPROVED))); assertTrue(updatedApprovals.contains(new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("openid") .setExpiresAt(Approval.timeFromNow(2000)) .setStatus(DENIED))); assertTrue(updatedApprovals.contains(new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("cloud_controller.read") .setExpiresAt(Approval.timeFromNow(2000)) .setStatus(APPROVED))); assertTrue(updatedApprovals.contains(new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("uaa.user") .setExpiresAt(Approval.timeFromNow(2000)) .setStatus(APPROVED))); } public void attemptingToCreateDuplicateApprovalsExtendsValidity() { addApproval(marissa.getId(), "c1", "uaa.user", 6000, APPROVED); addApproval(marissa.getId(), "c1", "uaa.admin", 12000, DENIED); addApproval(marissa.getId(), "c1", "openid", 6000, APPROVED); addApproval(marissa.getId(), "c1", "openid", 10000, APPROVED); List<Approval> updatedApprovals = endpoints.getApprovals("user_id eq \""+marissa.getId()+"\"", 1, 100); assertEquals(3, updatedApprovals.size()); assertTrue(updatedApprovals.contains(new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("uaa.user") .setExpiresAt(Approval.timeFromNow(6000)) .setStatus(APPROVED))); assertTrue(updatedApprovals.contains(new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("uaa.admin") .setExpiresAt(Approval.timeFromNow(12000)) .setStatus(DENIED))); assertTrue(updatedApprovals.contains(new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("openid") .setExpiresAt(Approval.timeFromNow(10000)) .setStatus(APPROVED))); } public void attemptingToCreateAnApprovalWithADifferentStatusUpdatesApproval() { addApproval(marissa.getId(), "c1", "uaa.user", 6000, APPROVED); addApproval(marissa.getId(), "c1", "uaa.admin", 12000, DENIED); addApproval(marissa.getId(), "c1", "openid", 6000, APPROVED); addApproval(marissa.getId(), "c1", "openid", 18000, DENIED); List<Approval> updatedApprovals = endpoints.getApprovals("user_id eq \""+marissa.getId()+"\"", 1, 100); assertEquals(4, updatedApprovals.size()); assertTrue(updatedApprovals.contains(new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("uaa.user") .setExpiresAt(Approval.timeFromNow(6000)) .setStatus(APPROVED))); assertTrue(updatedApprovals.contains(new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("uaa.admin") .setExpiresAt(Approval.timeFromNow(12000)) .setStatus(DENIED))); assertTrue(updatedApprovals.contains(new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("openid") .setExpiresAt(Approval.timeFromNow(18000)) .setStatus(DENIED))); } @Test(expected = UaaException.class) public void userCannotUpdateApprovalsForAnotherUser() { addApproval(marissa.getId(), "c1", "uaa.user", 6000, APPROVED); addApproval(marissa.getId(), "c1", "uaa.admin", 12000, DENIED); addApproval(marissa.getId(), "c1", "openid", 6000, APPROVED); endpoints.setSecurityContextAccessor(mockSecurityContextAccessor("vidya", "123456")); endpoints.updateApprovals(new Approval[] {new Approval() .setUserId(marissa.getId()) .setClientId("c1") .setScope("uaa.user") .setExpiresAt(Approval.timeFromNow(2000)) .setStatus(APPROVED)}); } @Test public void canRevokeApprovals() { addApproval(marissa.getId(), "c1", "uaa.user", 6000, APPROVED); addApproval(marissa.getId(), "c1", "uaa.admin", 12000, DENIED); addApproval(marissa.getId(), "c1", "openid", 6000, APPROVED); assertEquals(3, endpoints.getApprovals("user_id pr", 1, 100).size()); assertEquals("ok", endpoints.revokeApprovals("c1").getStatus()); assertEquals(0, endpoints.getApprovals("user_id pr", 1, 100).size()); } @Test @Ignore("Running locally only, to determine if the solution was feasible.") public void performance_is_acceptable() throws Exception { int max = 200000; rebuildIndices(); int delta = 20; for (int i = 0; i<delta; i++) { int count = (max / delta); int start = i*count; doWithTiming("addUsers", start, count); doWithTiming("addApprovals", start, start+count, 5); } assertThat(doWithTiming("getApprovalsCount", "user_id eq \"user-1000\""), lessThan(5d) ); assertThat(doWithTiming("getApprovalsCount", "client_id eq \"c1\""), lessThan(5d) ); dao.setHandleRevocationsAsExpiry(true); assertThat(doWithTiming("revokeApprovalsCount", "user_id eq \"user-1000\""), lessThan(5d) ); assertThat(doWithTiming("revokeApprovalsCount", "client_id eq \"c1\""), lessThan(5d) ); dao.setHandleRevocationsAsExpiry(false); assertThat(doWithTiming("revokeApprovalsCount", "user_id eq \"user-1001\""), lessThan(5d) ); assertThat(doWithTiming("revokeApprovalsCount", "client_id eq \"c2\""), lessThan(5d) ); } public void revokeApprovalsCountForUser(String userId) { assertTrue(dao.revokeApprovalsForClient(userId)); } public void revokeApprovalsCountForClient(String clientId) { assertTrue(dao.revokeApprovalsForClient(clientId)); } public void revokeApprovalsCountForClientAndUser(String clientId, String userId) { assertTrue(dao.revokeApprovalsForClientAndUser(clientId, userId)); } public int getApprovalsCountForUser(String userId) { return dao.getApprovalsForUser(userId).size(); } public int getApprovalsCountForClient(String clientId) { return dao.getApprovalsForClient(clientId).size(); } public int getApprovalsCount(String clientId, String userId) { return dao.getApprovals(userId, clientId).size(); } public void rebuildIndices() { sqlNoError("OPTIMIZE TABLE users"); sqlNoError("OPTIMIZE TABLE authz_approvals"); sqlNoError("REINDEX TABLE users"); sqlNoError("REINDEX TABLE authz_approvals"); sqlNoError("DBCC DBREINDEX ('users')"); sqlNoError("DBCC DBREINDEX ('authz_approvals')"); } public void sqlNoError(String sql) { try { jdbcTemplate.update(sql); System.err.println("Succeeded: "+sql); } catch (Exception e) { System.err.println("Failed: "+sql); } } public double doWithTiming(String methodName, Object... args) throws Exception { Method method = this.getClass().getMethod(methodName, Arrays.stream(args).map(a -> a.getClass()).collect(Collectors.toList()).toArray(new Class[0])); double start = System.currentTimeMillis(); method.invoke(this, args); double stop = System.currentTimeMillis(); double timing = (stop - start) / 1000d; System.err.println(String.format("\nPerformed %s(%s) in %.4f seconds", methodName, Arrays.toString(args), timing)); return timing; } public void addUsers(final Integer startIndex, final Integer size) throws Exception { jdbcTemplate.batchUpdate(INSERT_BARE_BONE_USER, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { String userId = "user-"+(i+startIndex); int pos = 1; ps.setString(pos++, userId); ps.setString(pos++, userId); ps.setString(pos++, userId); ps.setString(pos++, userId + "@test.com"); ps.setString(pos++, IdentityZoneHolder.get().getId()); } @Override public int getBatchSize() { return size; } }); } public void addApprovals(final Integer minUserId, final Integer maxUserId, final Integer countPerUser) throws Exception { jdbcTemplate.batchUpdate("insert into authz_approvals (user_id, client_id, scope, expiresat, status, lastmodifiedat) values (?,?,?,?,?,?)", new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { int index = (i+minUserId) / countPerUser; String userId = "user-"+(minUserId+index); int pos = 1; ps.setString(pos++, userId); ps.setString(pos++, "c"+random.nextInt(200)); ps.setString(pos++, "uaa.user."+i); ps.setTimestamp(pos++, new Timestamp(System.currentTimeMillis()+300000)); ps.setString(pos++, "APPROVED"); ps.setTimestamp(pos++, new Timestamp(System.currentTimeMillis())); } @Override public int getBatchSize() { return (maxUserId - minUserId) * countPerUser; } }); } }