KEYCLOAK-3003 Support for admin events in AuthenticationManagementResource

This commit is contained in:
mposolda 2016-05-24 18:28:48 +02:00
parent 5ed09acd94
commit f58936025f
11 changed files with 252 additions and 49 deletions

View file

@ -25,9 +25,18 @@ package org.keycloak.representations.idm;
*/ */
public class RequiredActionProviderSimpleRepresentation { public class RequiredActionProviderSimpleRepresentation {
private String id;
private String name; private String name;
private String providerId; private String providerId;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() { public String getName() {
return name; return name;
} }

View file

@ -28,6 +28,7 @@ import org.keycloak.authentication.FormAction;
import org.keycloak.authentication.FormAuthenticator; import org.keycloak.authentication.FormAuthenticator;
import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.AuthenticatorConfigModel;
@ -212,7 +213,10 @@ public class AuthenticationManagementResource {
return ErrorResponse.exists("Flow " + flow.getAlias() + " already exists"); return ErrorResponse.exists("Flow " + flow.getAlias() + " already exists");
} }
realm.addAuthenticationFlow(RepresentationToModel.toModel(flow)); AuthenticationFlowModel createdModel = realm.addAuthenticationFlow(RepresentationToModel.toModel(flow));
flow.setId(createdModel.getId());
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, createdModel.getId()).representation(flow).success();
return Response.status(201).build(); return Response.status(201).build();
} }
@ -263,6 +267,9 @@ public class AuthenticationManagementResource {
realm.removeAuthenticatorExecution(execution); realm.removeAuthenticatorExecution(execution);
} }
realm.removeAuthenticationFlow(flow); realm.removeAuthenticationFlow(flow);
// Use just one event for top-level flow. Using separate events won't work properly for flows of depth 2 or bigger
adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
} }
/** /**
@ -299,6 +306,9 @@ public class AuthenticationManagementResource {
copy = realm.addAuthenticationFlow(copy); copy = realm.addAuthenticationFlow(copy);
copy(newName, flow, copy); copy(newName, flow, copy);
data.put("id", copy.getId());
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo).representation(data).success();
return Response.status(201).build(); return Response.status(201).build();
} }
@ -362,8 +372,10 @@ public class AuthenticationManagementResource {
execution.setAuthenticatorFlow(true); execution.setAuthenticatorFlow(true);
execution.setAuthenticator(provider); execution.setAuthenticator(provider);
execution.setPriority(getNextPriority(parentFlow)); execution.setPriority(getNextPriority(parentFlow));
execution = realm.addAuthenticatorExecution(execution);
realm.addAuthenticatorExecution(execution); data.put("id", execution.getId());
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo).representation(data).success();
} }
private int getNextPriority(AuthenticationFlowModel parentFlow) { private int getNextPriority(AuthenticationFlowModel parentFlow) {
@ -413,7 +425,10 @@ public class AuthenticationManagementResource {
execution.setAuthenticator(provider); execution.setAuthenticator(provider);
execution.setPriority(getNextPriority(parentFlow)); execution.setPriority(getNextPriority(parentFlow));
realm.addAuthenticatorExecution(execution); execution = realm.addAuthenticatorExecution(execution);
data.put("id", execution.getId());
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo).representation(data).success();
} }
/** /**
@ -519,6 +534,7 @@ public class AuthenticationManagementResource {
if (!model.getRequirement().name().equals(rep.getRequirement())) { if (!model.getRequirement().name().equals(rep.getRequirement())) {
model.setRequirement(AuthenticationExecutionModel.Requirement.valueOf(rep.getRequirement())); model.setRequirement(AuthenticationExecutionModel.Requirement.valueOf(rep.getRequirement()));
realm.updateAuthenticatorExecution(model); realm.updateAuthenticatorExecution(model);
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
} }
} }
@ -541,6 +557,8 @@ public class AuthenticationManagementResource {
} }
model.setPriority(getNextPriority(parentFlow)); model.setPriority(getNextPriority(parentFlow));
model = realm.addAuthenticatorExecution(model); model = realm.addAuthenticatorExecution(model);
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, model.getId()).representation(execution).success();
return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build(); return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build();
} }
@ -592,6 +610,8 @@ public class AuthenticationManagementResource {
realm.updateAuthenticatorExecution(previous); realm.updateAuthenticatorExecution(previous);
model.setPriority(tmp); model.setPriority(tmp);
realm.updateAuthenticatorExecution(model); realm.updateAuthenticatorExecution(model);
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).success();
} }
public List<AuthenticationExecutionModel> getSortedExecutions(AuthenticationFlowModel parentFlow) { public List<AuthenticationExecutionModel> getSortedExecutions(AuthenticationFlowModel parentFlow) {
@ -635,6 +655,8 @@ public class AuthenticationManagementResource {
realm.updateAuthenticatorExecution(model); realm.updateAuthenticatorExecution(model);
next.setPriority(tmp); next.setPriority(tmp);
realm.updateAuthenticatorExecution(next); realm.updateAuthenticatorExecution(next);
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).success();
} }
@ -666,6 +688,8 @@ public class AuthenticationManagementResource {
} }
realm.removeAuthenticatorExecution(model); realm.removeAuthenticatorExecution(model);
adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
} }
@ -693,6 +717,9 @@ public class AuthenticationManagementResource {
config = realm.addAuthenticatorConfig(config); config = realm.addAuthenticatorConfig(config);
model.setAuthenticatorConfig(config.getId()); model.setAuthenticatorConfig(config.getId());
realm.updateAuthenticatorExecution(model); realm.updateAuthenticatorExecution(model);
json.setId(config.getId());
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo).representation(json).success();
return Response.created(uriInfo.getAbsolutePathBuilder().path(config.getId()).build()).build(); return Response.created(uriInfo.getAbsolutePathBuilder().path(config.getId()).build()).build();
} }
@ -772,7 +799,10 @@ public class AuthenticationManagementResource {
requiredAction.setProviderId(providerId); requiredAction.setProviderId(providerId);
requiredAction.setDefaultAction(false); requiredAction.setDefaultAction(false);
requiredAction.setEnabled(true); requiredAction.setEnabled(true);
realm.addRequiredActionProvider(requiredAction); requiredAction = realm.addRequiredActionProvider(requiredAction);
data.put("id", requiredAction.getId());
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo).representation(data).success();
} }
@ -850,6 +880,8 @@ public class AuthenticationManagementResource {
update.setEnabled(rep.isEnabled()); update.setEnabled(rep.isEnabled());
update.setConfig(rep.getConfig()); update.setConfig(rep.getConfig());
realm.updateRequiredActionProvider(update); realm.updateRequiredActionProvider(update);
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
} }
/** /**
@ -866,6 +898,8 @@ public class AuthenticationManagementResource {
throw new NotFoundException("Failed to find required action."); throw new NotFoundException("Failed to find required action.");
} }
realm.removeRequiredActionProvider(model); realm.removeRequiredActionProvider(model);
adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
} }
/** /**
@ -947,6 +981,7 @@ public class AuthenticationManagementResource {
auth.requireManage(); auth.requireManage();
AuthenticatorConfigModel config = realm.addAuthenticatorConfig(RepresentationToModel.toModel(rep)); AuthenticatorConfigModel config = realm.addAuthenticatorConfig(RepresentationToModel.toModel(rep));
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, config.getId()).representation(rep).success();
return Response.created(uriInfo.getAbsolutePathBuilder().path(config.getId()).build()).build(); return Response.created(uriInfo.getAbsolutePathBuilder().path(config.getId()).build()).build();
} }
@ -995,6 +1030,8 @@ public class AuthenticationManagementResource {
} }
realm.removeAuthenticatorConfig(config); realm.removeAuthenticatorConfig(config);
adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
} }
/** /**
@ -1017,5 +1054,6 @@ public class AuthenticationManagementResource {
exists.setAlias(rep.getAlias()); exists.setAlias(rep.getAlias());
exists.setConfig(rep.getConfig()); exists.setConfig(rep.getConfig());
realm.updateAuthenticatorConfig(exists); realm.updateAuthenticatorConfig(exists);
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
} }
} }

View file

@ -19,19 +19,26 @@ package org.keycloak.testsuite.admin.authentication;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.keycloak.admin.client.resource.AuthenticationManagementResource; import org.keycloak.admin.client.resource.AuthenticationManagementResource;
import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.events.admin.OperationType;
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.util.AdminEventPaths;
import org.keycloak.testsuite.util.AssertAdminEvents;
import org.keycloak.testsuite.util.RealmBuilder;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import javax.ws.rs.core.Response;
/** /**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
@ -48,6 +55,9 @@ public abstract class AbstractAuthenticationTest extends AbstractKeycloakTest {
RealmResource realmResource; RealmResource realmResource;
AuthenticationManagementResource authMgmtResource; AuthenticationManagementResource authMgmtResource;
@Rule
public AssertAdminEvents assertAdminEvents = new AssertAdminEvents(this);
@Before @Before
public void before() { public void before() {
realmResource = adminClient.realms().realm(REALM_NAME); realmResource = adminClient.realms().realm(REALM_NAME);
@ -56,9 +66,8 @@ public abstract class AbstractAuthenticationTest extends AbstractKeycloakTest {
@Override @Override
public void addTestRealms(List<RealmRepresentation> testRealms) { public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation testRealmRep = new RealmRepresentation(); RealmRepresentation testRealmRep = RealmBuilder.create().name(REALM_NAME).testEventListener().build();
testRealmRep.setRealm(REALM_NAME); testRealmRep.setId(REALM_NAME);
testRealmRep.setEnabled(true);
testRealms.add(testRealmRep); testRealms.add(testRealmRep);
} }
@ -182,4 +191,11 @@ public abstract class AbstractAuthenticationTest extends AbstractKeycloakTest {
config.setConfig(params); config.setConfig(params);
return config; return config;
} }
void createFlow(AuthenticationFlowRepresentation flowRep) {
Response response = authMgmtResource.createFlow(flowRep);
org.keycloak.testsuite.Assert.assertEquals(201, response.getStatus());
response.close();
assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AssertAdminEvents.isExpectedPrefixFollowedByUuid(AdminEventPaths.authFlowsPath()), flowRep);
}
} }

View file

@ -28,11 +28,14 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthenticator; import org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthenticator;
import org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthenticatorFactory; import org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthenticatorFactory;
import org.keycloak.events.admin.OperationType;
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.util.AdminEventPaths;
import org.keycloak.testsuite.util.AssertAdminEvents;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -43,13 +46,13 @@ public class AuthenticatorConfigTest extends AbstractAuthenticationTest {
@Before @Before
public void beforeConfigTest() { public void beforeConfigTest() {
Response response = authMgmtResource.createFlow(newFlow("firstBrokerLogin2", "firstBrokerLogin2", "basic-flow", true, false)); AuthenticationFlowRepresentation flowRep = newFlow("firstBrokerLogin2", "firstBrokerLogin2", "basic-flow", true, false);
Assert.assertEquals(201, response.getStatus()); createFlow(flowRep);
response.close();
HashMap<String, String> params = new HashMap<>(); HashMap<String, String> params = new HashMap<>();
params.put("provider", IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID); params.put("provider", IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID);
authMgmtResource.addExecution("firstBrokerLogin2", params); authMgmtResource.addExecution("firstBrokerLogin2", params);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AdminEventPaths.authAddExecutionPath("firstBrokerLogin2"), params);
List<AuthenticationExecutionInfoRepresentation> executionReps = authMgmtResource.getExecutions("firstBrokerLogin2"); List<AuthenticationExecutionInfoRepresentation> executionReps = authMgmtResource.getExecutions("firstBrokerLogin2");
AuthenticationExecutionInfoRepresentation exec = findExecutionByProvider(IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID, executionReps); AuthenticationExecutionInfoRepresentation exec = findExecutionByProvider(IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID, executionReps);
@ -57,12 +60,6 @@ public class AuthenticatorConfigTest extends AbstractAuthenticationTest {
executionId = exec.getId(); executionId = exec.getId();
} }
@Override
public void afterAbstractKeycloakTest() {
AuthenticationFlowRepresentation flowRep = findFlowByAlias("firstBrokerLogin2", authMgmtResource.getFlows());
authMgmtResource.deleteFlow(flowRep.getId());
}
@Test @Test
public void testCreateConfig() { public void testCreateConfig() {
@ -82,6 +79,7 @@ public class AuthenticatorConfigTest extends AbstractAuthenticationTest {
// Cleanup // Cleanup
authMgmtResource.removeAuthenticatorConfig(cfgId); authMgmtResource.removeAuthenticatorConfig(cfgId);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.DELETE, AdminEventPaths.authExecutionConfigPath(cfgId));
} }
@ -107,6 +105,7 @@ public class AuthenticatorConfigTest extends AbstractAuthenticationTest {
cfgRep.setAlias("foo2"); cfgRep.setAlias("foo2");
cfgRep.getConfig().put("configKey2", "configValue2"); cfgRep.getConfig().put("configKey2", "configValue2");
authMgmtResource.updateAuthenticatorConfig(cfgRep.getId(), cfgRep); authMgmtResource.updateAuthenticatorConfig(cfgRep.getId(), cfgRep);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.UPDATE, AdminEventPaths.authExecutionConfigPath(cfgId), cfgRep);
// Assert updated // Assert updated
cfgRep = authMgmtResource.getAuthenticatorConfig(cfgRep.getId()); cfgRep = authMgmtResource.getAuthenticatorConfig(cfgRep.getId());
@ -137,7 +136,8 @@ public class AuthenticatorConfigTest extends AbstractAuthenticationTest {
} }
// Test remove our config // Test remove our config
authMgmtResource.removeAuthenticatorConfig(cfgRep.getId()); authMgmtResource.removeAuthenticatorConfig(cfgId);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.DELETE, AdminEventPaths.authExecutionConfigPath(cfgId));
// Assert config not found // Assert config not found
try { try {
@ -159,6 +159,7 @@ public class AuthenticatorConfigTest extends AbstractAuthenticationTest {
Assert.assertEquals(201, resp.getStatus()); Assert.assertEquals(201, resp.getStatus());
String cfgId = ApiUtil.getCreatedId(resp); String cfgId = ApiUtil.getCreatedId(resp);
Assert.assertNotNull(cfgId); Assert.assertNotNull(cfgId);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AdminEventPaths.authAddExecutionConfigPath(executionId), cfg);
return cfgId; return cfgId;
} }

View file

@ -21,10 +21,12 @@ import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.keycloak.authentication.AuthenticationFlow; import org.keycloak.authentication.AuthenticationFlow;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator; import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.events.admin.OperationType;
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
import org.keycloak.representations.idm.AuthenticationExecutionRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.testsuite.util.AdminEventPaths;
import org.keycloak.testsuite.util.AssertAdminEvents;
import javax.ws.rs.BadRequestException; import javax.ws.rs.BadRequestException;
import javax.ws.rs.NotFoundException; import javax.ws.rs.NotFoundException;
@ -60,8 +62,9 @@ public class ExecutionTest extends AbstractAuthenticationTest {
} }
// copy built-in flow so we get a new editable flow // copy built-in flow so we get a new editable flow
params.put("newName", "Copy of browser"); params.put("newName", "Copy-of-browser");
Response response = authMgmtResource.copy("browser", params); Response response = authMgmtResource.copy("browser", params);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AdminEventPaths.authCopyFlowPath("browser"), params);
try { try {
Assert.assertEquals("Copy flow", 201, response.getStatus()); Assert.assertEquals("Copy flow", 201, response.getStatus());
} finally { } finally {
@ -71,17 +74,19 @@ public class ExecutionTest extends AbstractAuthenticationTest {
// add execution using inexistent provider // add execution using inexistent provider
params.put("provider", "test-execution"); params.put("provider", "test-execution");
try { try {
authMgmtResource.addExecution("Copy of browser", params); authMgmtResource.addExecution("CopyOfBrowser", params);
Assert.fail("add execution with inexistent provider should fail"); Assert.fail("add execution with inexistent provider should fail");
} catch(BadRequestException expected) { } catch(BadRequestException expected) {
// Expected
} }
// add execution - should succeed // add execution - should succeed
params.put("provider", "idp-review-profile"); params.put("provider", "idp-review-profile");
authMgmtResource.addExecution("Copy of browser", params); authMgmtResource.addExecution("Copy-of-browser", params);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AdminEventPaths.authAddExecutionPath("Copy-of-browser"), params);
// check execution was added // check execution was added
List<AuthenticationExecutionInfoRepresentation> executionReps = authMgmtResource.getExecutions("Copy of browser"); List<AuthenticationExecutionInfoRepresentation> executionReps = authMgmtResource.getExecutions("Copy-of-browser");
AuthenticationExecutionInfoRepresentation exec = findExecutionByProvider("idp-review-profile", executionReps); AuthenticationExecutionInfoRepresentation exec = findExecutionByProvider("idp-review-profile", executionReps);
Assert.assertNotNull("idp-review-profile added", exec); Assert.assertNotNull("idp-review-profile added", exec);
@ -92,9 +97,10 @@ public class ExecutionTest extends AbstractAuthenticationTest {
// remove execution // remove execution
authMgmtResource.removeExecution(exec.getId()); authMgmtResource.removeExecution(exec.getId());
assertAdminEvents.assertEvent(REALM_NAME, OperationType.DELETE, AdminEventPaths.authExecutionPath(exec.getId()));
// check execution was removed // check execution was removed
executionReps = authMgmtResource.getExecutions("Copy of browser"); executionReps = authMgmtResource.getExecutions("Copy-of-browser");
exec = findExecutionByProvider("idp-review-profile", executionReps); exec = findExecutionByProvider("idp-review-profile", executionReps);
Assert.assertNull("idp-review-profile removed", exec); Assert.assertNull("idp-review-profile removed", exec);
@ -102,6 +108,7 @@ public class ExecutionTest extends AbstractAuthenticationTest {
// delete auth-cookie // delete auth-cookie
authMgmtResource.removeExecution(authCookieExec.getId()); authMgmtResource.removeExecution(authCookieExec.getId());
assertAdminEvents.assertEvent(REALM_NAME, OperationType.DELETE, AdminEventPaths.authExecutionPath(authCookieExec.getId()));
AuthenticationExecutionRepresentation rep = new AuthenticationExecutionRepresentation(); AuthenticationExecutionRepresentation rep = new AuthenticationExecutionRepresentation();
rep.setPriority(10); rep.setPriority(10);
@ -135,13 +142,14 @@ public class ExecutionTest extends AbstractAuthenticationTest {
response.close(); response.close();
} }
// get Copy of browser flow id, and set it on execution // get Copy-of-browser flow id, and set it on execution
List<AuthenticationFlowRepresentation> flows = authMgmtResource.getFlows(); List<AuthenticationFlowRepresentation> flows = authMgmtResource.getFlows();
AuthenticationFlowRepresentation flow = findFlowByAlias("Copy of browser", flows); AuthenticationFlowRepresentation flow = findFlowByAlias("Copy-of-browser", flows);
rep.setParentFlow(flow.getId()); rep.setParentFlow(flow.getId());
// add execution - should succeed // add execution - should succeed
response = authMgmtResource.addExecution(rep); response = authMgmtResource.addExecution(rep);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AssertAdminEvents.isExpectedPrefixFollowedByUuid(AdminEventPaths.authMgmtBasePath() + "/executions"), rep);
try { try {
Assert.assertEquals("added execution", 201, response.getStatus()); Assert.assertEquals("added execution", 201, response.getStatus());
} finally { } finally {
@ -149,7 +157,7 @@ public class ExecutionTest extends AbstractAuthenticationTest {
} }
// check execution was added // check execution was added
List<AuthenticationExecutionInfoRepresentation> executions = authMgmtResource.getExecutions("Copy of browser"); List<AuthenticationExecutionInfoRepresentation> executions = authMgmtResource.getExecutions("Copy-of-browser");
exec = findExecutionByProvider("auth-cookie", executions); exec = findExecutionByProvider("auth-cookie", executions);
Assert.assertNotNull("auth-cookie added", exec); Assert.assertNotNull("auth-cookie added", exec);
@ -170,6 +178,7 @@ public class ExecutionTest extends AbstractAuthenticationTest {
// switch from DISABLED to ALTERNATIVE // switch from DISABLED to ALTERNATIVE
exec.setRequirement(DISABLED); exec.setRequirement(DISABLED);
authMgmtResource.updateExecutions("browser", exec); authMgmtResource.updateExecutions("browser", exec);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.UPDATE, AdminEventPaths.authUpdateExecutionPath("browser"), exec);
// make sure the change is visible // make sure the change is visible
executionReps = authMgmtResource.getExecutions("browser"); executionReps = authMgmtResource.getExecutions("browser");
@ -183,14 +192,13 @@ public class ExecutionTest extends AbstractAuthenticationTest {
public void testClientFlowExecutions() { public void testClientFlowExecutions() {
// Create client flow // Create client flow
AuthenticationFlowRepresentation clientFlow = newFlow("new-client-flow", "desc", AuthenticationFlow.CLIENT_FLOW, true, false); AuthenticationFlowRepresentation clientFlow = newFlow("new-client-flow", "desc", AuthenticationFlow.CLIENT_FLOW, true, false);
Response response = authMgmtResource.createFlow(clientFlow); createFlow(clientFlow);
Assert.assertEquals(201, response.getStatus());
response.close();
// Add execution to it // Add execution to it
Map<String, String> executionData = new HashMap<>(); Map<String, String> executionData = new HashMap<>();
executionData.put("provider", ClientIdAndSecretAuthenticator.PROVIDER_ID); executionData.put("provider", ClientIdAndSecretAuthenticator.PROVIDER_ID);
authMgmtResource.addExecution("new-client-flow", executionData); authMgmtResource.addExecution("new-client-flow", executionData);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AdminEventPaths.authAddExecutionPath("new-client-flow"), executionData);
// Check executions of not-existent flow - SHOULD FAIL // Check executions of not-existent flow - SHOULD FAIL
try { try {
@ -226,6 +234,7 @@ public class ExecutionTest extends AbstractAuthenticationTest {
// Update success // Update success
executionRep.setRequirement(ALTERNATIVE); executionRep.setRequirement(ALTERNATIVE);
authMgmtResource.updateExecutions("new-client-flow", executionRep); authMgmtResource.updateExecutions("new-client-flow", executionRep);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.UPDATE, AdminEventPaths.authUpdateExecutionPath("new-client-flow"), executionRep);
// Check updated // Check updated
executionRep = findExecutionByProvider(ClientIdAndSecretAuthenticator.PROVIDER_ID, authMgmtResource.getExecutions("new-client-flow")); executionRep = findExecutionByProvider(ClientIdAndSecretAuthenticator.PROVIDER_ID, authMgmtResource.getExecutions("new-client-flow"));
@ -241,7 +250,10 @@ public class ExecutionTest extends AbstractAuthenticationTest {
// Successfuly remove execution and flow // Successfuly remove execution and flow
authMgmtResource.removeExecution(executionRep.getId()); authMgmtResource.removeExecution(executionRep.getId());
assertAdminEvents.assertEvent(REALM_NAME, OperationType.DELETE, AdminEventPaths.authExecutionPath(executionRep.getId()));
AuthenticationFlowRepresentation rep = findFlowByAlias("new-client-flow", authMgmtResource.getFlows()); AuthenticationFlowRepresentation rep = findFlowByAlias("new-client-flow", authMgmtResource.getFlows());
authMgmtResource.deleteFlow(rep.getId()); authMgmtResource.deleteFlow(rep.getId());
assertAdminEvents.assertEvent(REALM_NAME, OperationType.DELETE, AdminEventPaths.authFlowPath(rep.getId()));
} }
} }

View file

@ -19,8 +19,10 @@ package org.keycloak.testsuite.admin.authentication;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.keycloak.events.admin.OperationType;
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.testsuite.util.AdminEventPaths;
import javax.ws.rs.BadRequestException; import javax.ws.rs.BadRequestException;
import javax.ws.rs.NotFoundException; import javax.ws.rs.NotFoundException;
@ -67,12 +69,7 @@ public class FlowTest extends AbstractAuthenticationTest {
// create new flow that should succeed // create new flow that should succeed
AuthenticationFlowRepresentation newFlow = newFlow("browser-2", "Browser flow", "basic-flow", true, false); AuthenticationFlowRepresentation newFlow = newFlow("browser-2", "Browser flow", "basic-flow", true, false);
response = authMgmtResource.createFlow(newFlow); createFlow(newFlow);
try {
Assert.assertEquals("createFlow success", 201, response.getStatus());
} finally {
response.close();
}
// check that new flow is returned in a children list // check that new flow is returned in a children list
flows = authMgmtResource.getFlows(); flows = authMgmtResource.getFlows();
@ -122,6 +119,7 @@ public class FlowTest extends AbstractAuthenticationTest {
// Successfully add flow // Successfully add flow
data.put("alias", "SomeFlow"); data.put("alias", "SomeFlow");
authMgmtResource.addExecutionFlow("browser-2", data); authMgmtResource.addExecutionFlow("browser-2", data);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AdminEventPaths.authAddExecutionFlowPath("browser-2"), data);
// check that new flow is returned in a children list // check that new flow is returned in a children list
flows = authMgmtResource.getFlows(); flows = authMgmtResource.getFlows();
@ -143,6 +141,7 @@ public class FlowTest extends AbstractAuthenticationTest {
// delete non-built-in flow // delete non-built-in flow
authMgmtResource.deleteFlow(found.getId()); authMgmtResource.deleteFlow(found.getId());
assertAdminEvents.assertEvent(REALM_NAME, OperationType.DELETE, AdminEventPaths.authFlowPath(found.getId()));
// check the deleted flow is no longer returned // check the deleted flow is no longer returned
flows = authMgmtResource.getFlows(); flows = authMgmtResource.getFlows();
@ -185,6 +184,7 @@ public class FlowTest extends AbstractAuthenticationTest {
// copy that should succeed // copy that should succeed
params.put("newName", "Copy of browser"); params.put("newName", "Copy of browser");
response = authMgmtResource.copy("browser", params); response = authMgmtResource.copy("browser", params);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AdminEventPaths.authCopyFlowPath("browser"), params);
try { try {
Assert.assertEquals("Copy flow", 201, response.getStatus()); Assert.assertEquals("Copy flow", 201, response.getStatus());
} finally { } finally {
@ -219,6 +219,7 @@ public class FlowTest extends AbstractAuthenticationTest {
Response response = authMgmtResource.copy("browser", params); Response response = authMgmtResource.copy("browser", params);
Assert.assertEquals(201, response.getStatus()); Assert.assertEquals(201, response.getStatus());
response.close(); response.close();
assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AdminEventPaths.authCopyFlowPath("browser"), params);
params = new HashMap<>(); params = new HashMap<>();
params.put("alias", "child"); params.put("alias", "child");
@ -227,6 +228,7 @@ public class FlowTest extends AbstractAuthenticationTest {
params.put("type", "basic-flow"); params.put("type", "basic-flow");
authMgmtResource.addExecutionFlow("parent", params); authMgmtResource.addExecutionFlow("parent", params);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AdminEventPaths.authAddExecutionFlowPath("parent"), params);
} }
} }

View file

@ -25,6 +25,9 @@ import javax.ws.rs.core.Response;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.keycloak.events.admin.OperationType;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.testsuite.util.AdminEventPaths;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -34,13 +37,8 @@ public class RegistrationFlowTest extends AbstractAuthenticationTest {
@Test @Test
public void testAddExecution() { public void testAddExecution() {
// Add registration flow 2 // Add registration flow 2
Response response = authMgmtResource.createFlow(newFlow("registration2", "RegistrationFlow2", "basic-flow", true, false)); AuthenticationFlowRepresentation flowRep = newFlow("registration2", "RegistrationFlow2", "basic-flow", true, false);
try { createFlow(flowRep);
Assert.assertEquals("createFlow success", 201, response.getStatus());
} finally {
response.close();
}
// add registration execution form flow // add registration execution form flow
Map<String, String> data = new HashMap<>(); Map<String, String> data = new HashMap<>();
@ -49,6 +47,7 @@ public class RegistrationFlowTest extends AbstractAuthenticationTest {
data.put("description", "registrationForm2 flow"); data.put("description", "registrationForm2 flow");
data.put("provider", "registration-page-form"); data.put("provider", "registration-page-form");
authMgmtResource.addExecutionFlow("registration2", data); authMgmtResource.addExecutionFlow("registration2", data);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AdminEventPaths.authAddExecutionFlowPath("registration2"), data);
// Should fail to add execution under top level flow // Should fail to add execution under top level flow
Map<String, String> data2 = new HashMap<>(); Map<String, String> data2 = new HashMap<>();
@ -63,9 +62,9 @@ public class RegistrationFlowTest extends AbstractAuthenticationTest {
// Should success to add execution under form flow // Should success to add execution under form flow
authMgmtResource.addExecution("registrationForm2", data2); authMgmtResource.addExecution("registrationForm2", data2);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AdminEventPaths.authAddExecutionPath("registrationForm2"), data2);
} }
// TODO: More coverage... And hopefully more type-safety instead of passing generic maps // TODO: More type-safety instead of passing generic maps
} }

View file

@ -19,9 +19,11 @@ package org.keycloak.testsuite.admin.authentication;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.keycloak.events.admin.OperationType;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation; import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation; import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation;
import org.keycloak.testsuite.actions.DummyRequiredActionFactory; import org.keycloak.testsuite.actions.DummyRequiredActionFactory;
import org.keycloak.testsuite.util.AdminEventPaths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -59,6 +61,7 @@ public class RequiredActionsTest extends AbstractAuthenticationTest {
forUpdate.setConfig(Collections.<String, String>emptyMap()); forUpdate.setConfig(Collections.<String, String>emptyMap());
authMgmtResource.updateRequiredAction(forUpdate.getAlias(), forUpdate); authMgmtResource.updateRequiredAction(forUpdate.getAlias(), forUpdate);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.UPDATE, AdminEventPaths.authRequiredActionPath(forUpdate.getAlias()));
result = authMgmtResource.getRequiredActions(); result = authMgmtResource.getRequiredActions();
RequiredActionProviderRepresentation updated = findRequiredActionByAlias(forUpdate.getAlias(), result); RequiredActionProviderRepresentation updated = findRequiredActionByAlias(forUpdate.getAlias(), result);
@ -78,6 +81,7 @@ public class RequiredActionsTest extends AbstractAuthenticationTest {
// Register it // Register it
authMgmtResource.registerRequiredAction(action); authMgmtResource.registerRequiredAction(action);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AdminEventPaths.authMgmtBasePath() + "/register-required-action", action);
// Try to find not-existent action - should fail // Try to find not-existent action - should fail
try { try {
@ -103,6 +107,7 @@ public class RequiredActionsTest extends AbstractAuthenticationTest {
// Update (set it as defaultAction) // Update (set it as defaultAction)
rep.setDefaultAction(true); rep.setDefaultAction(true);
authMgmtResource.updateRequiredAction(DummyRequiredActionFactory.PROVIDER_ID, rep); authMgmtResource.updateRequiredAction(DummyRequiredActionFactory.PROVIDER_ID, rep);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.UPDATE, AdminEventPaths.authRequiredActionPath(rep.getAlias()), rep);
compareRequiredAction(rep, newRequiredAction(DummyRequiredActionFactory.PROVIDER_ID, "Dummy Action", compareRequiredAction(rep, newRequiredAction(DummyRequiredActionFactory.PROVIDER_ID, "Dummy Action",
true, true, Collections.emptyMap())); true, true, Collections.emptyMap()));
@ -116,6 +121,7 @@ public class RequiredActionsTest extends AbstractAuthenticationTest {
// Remove success // Remove success
authMgmtResource.removeRequiredAction(DummyRequiredActionFactory.PROVIDER_ID); authMgmtResource.removeRequiredAction(DummyRequiredActionFactory.PROVIDER_ID);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.DELETE, AdminEventPaths.authRequiredActionPath(rep.getAlias()));
} }

View file

@ -19,7 +19,9 @@ package org.keycloak.testsuite.admin.authentication;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.keycloak.events.admin.OperationType;
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
import org.keycloak.testsuite.util.AdminEventPaths;
import javax.ws.rs.NotFoundException; import javax.ws.rs.NotFoundException;
import javax.ws.rs.BadRequestException; import javax.ws.rs.BadRequestException;
@ -39,6 +41,7 @@ public class ShiftExecutionTest extends AbstractAuthenticationTest {
HashMap<String, String> params = new HashMap<>(); HashMap<String, String> params = new HashMap<>();
params.put("newName", "Copy of browser"); params.put("newName", "Copy of browser");
Response response = authMgmtResource.copy("browser", params); Response response = authMgmtResource.copy("browser", params);
assertAdminEvents.assertEvent(REALM_NAME, OperationType.CREATE, AdminEventPaths.authCopyFlowPath("browser"), params);
try { try {
Assert.assertEquals("Copy flow", 201, response.getStatus()); Assert.assertEquals("Copy flow", 201, response.getStatus());
} finally { } finally {
@ -61,6 +64,7 @@ public class ShiftExecutionTest extends AbstractAuthenticationTest {
// shift last execution up // shift last execution up
authMgmtResource.raisePriority(last.getId()); authMgmtResource.raisePriority(last.getId());
assertAdminEvents.assertEvent(REALM_NAME, OperationType.UPDATE, AdminEventPaths.authRaiseExecutionPath(last.getId()));
List<AuthenticationExecutionInfoRepresentation> executions2 = authMgmtResource.getExecutions("Copy of browser"); List<AuthenticationExecutionInfoRepresentation> executions2 = authMgmtResource.getExecutions("Copy of browser");
@ -80,6 +84,7 @@ public class ShiftExecutionTest extends AbstractAuthenticationTest {
// shift one before last down // shift one before last down
authMgmtResource.lowerPriority(oneButLast2.getId()); authMgmtResource.lowerPriority(oneButLast2.getId());
assertAdminEvents.assertEvent(REALM_NAME, OperationType.UPDATE, AdminEventPaths.authLowerExecutionPath(oneButLast2.getId()));
executions2 = authMgmtResource.getExecutions("Copy of browser"); executions2 = authMgmtResource.getExecutions("Copy of browser");

View file

@ -21,6 +21,7 @@ import java.net.URI;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import org.keycloak.admin.client.resource.AuthenticationManagementResource;
import org.keycloak.admin.client.resource.ClientAttributeCertificateResource; import org.keycloak.admin.client.resource.ClientAttributeCertificateResource;
import org.keycloak.admin.client.resource.ClientInitialAccessResource; import org.keycloak.admin.client.resource.ClientInitialAccessResource;
import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientResource;
@ -353,4 +354,83 @@ public class AdminEventPaths {
} }
// AUTHENTICATION FLOWS
public static String authMgmtBasePath() {
URI uri = UriBuilder.fromUri("").path(RealmResource.class, "flows")
.build();
return uri.toString();
}
public static String authFlowsPath() {
URI uri = UriBuilder.fromUri(authMgmtBasePath()).path(AuthenticationManagementResource.class, "getFlows")
.build();
return uri.toString();
}
public static String authFlowPath(String flowId) {
URI uri = UriBuilder.fromUri(authMgmtBasePath()).path(AuthenticationManagementResource.class, "getFlow")
.build(flowId);
return uri.toString();
}
public static String authCopyFlowPath(String flowAlias) {
URI uri = UriBuilder.fromUri(authMgmtBasePath()).path(AuthenticationManagementResource.class, "copy")
.build(flowAlias);
return uri.toString();
}
public static String authAddExecutionFlowPath(String flowAlias) {
URI uri = UriBuilder.fromUri(authMgmtBasePath()).path(AuthenticationManagementResource.class, "addExecutionFlow")
.build(flowAlias);
return uri.toString();
}
public static String authAddExecutionPath(String flowAlias) {
return authFlowPath(flowAlias) + "/executions/execution";
}
public static String authUpdateExecutionPath(String flowAlias) {
URI uri = UriBuilder.fromUri(authMgmtBasePath()).path(AuthenticationManagementResource.class, "updateExecutions")
.build(flowAlias);
return uri.toString();
}
public static String authExecutionPath(String executionId) {
URI uri = UriBuilder.fromUri(authMgmtBasePath()).path(AuthenticationManagementResource.class, "removeExecution")
.build(executionId);
return uri.toString();
}
public static String authAddExecutionConfigPath(String executionId) {
URI uri = UriBuilder.fromUri(authMgmtBasePath()).path(AuthenticationManagementResource.class, "newExecutionConfig")
.build(executionId);
return uri.toString();
}
public static String authExecutionConfigPath(String configId) {
URI uri = UriBuilder.fromUri(authMgmtBasePath()).path(AuthenticationManagementResource.class, "getAuthenticatorConfig")
.build(configId);
return uri.toString();
}
public static String authRaiseExecutionPath(String executionId) {
URI uri = UriBuilder.fromUri(authMgmtBasePath()).path(AuthenticationManagementResource.class, "raisePriority")
.build(executionId);
return uri.toString();
}
public static String authLowerExecutionPath(String executionId) {
URI uri = UriBuilder.fromUri(authMgmtBasePath()).path(AuthenticationManagementResource.class, "lowerPriority")
.build(executionId);
return uri.toString();
}
public static String authRequiredActionPath(String requiredActionAlias) {
URI uri = UriBuilder.fromUri(authMgmtBasePath()).path(AuthenticationManagementResource.class, "getRequiredAction")
.build(requiredActionAlias);
return uri.toString();
}
} }

View file

@ -19,11 +19,14 @@ package org.keycloak.testsuite.util;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Map;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import org.hamcrest.Description;
import org.hamcrest.Matcher; import org.hamcrest.Matcher;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.hamcrest.TypeSafeMatcher;
import org.junit.rules.TestRule; import org.junit.rules.TestRule;
import org.junit.runners.model.Statement; import org.junit.runners.model.Statement;
import org.keycloak.common.util.ObjectUtil; import org.keycloak.common.util.ObjectUtil;
@ -205,12 +208,27 @@ public class AssertAdminEvents implements TestRule {
try { try {
Object actualRep = JsonSerialization.readValue(actual.getRepresentation(), expectedRep.getClass()); Object actualRep = JsonSerialization.readValue(actual.getRepresentation(), expectedRep.getClass());
for (Method method : Reflections.getAllDeclaredMethods(expectedRep.getClass())) { if (expectedRep instanceof Map) {
if (method.getName().startsWith("get") || method.getName().startsWith("is")) { // Special comparing of representations of type map. All of "expected" must be available on "actual"
Object expectedValue = Reflections.invokeMethod(method, expectedRep); Map<?, ?> expectedRepMap = (Map) expectedRep;
Map<?, ?> actualRepMap = (Map) actualRep;
for (Map.Entry entry : expectedRepMap.entrySet()) {
Object expectedValue = entry.getValue();
if (expectedValue != null) { if (expectedValue != null) {
Object actualValue = Reflections.invokeMethod(method, actualRep); Object actualValue = actualRepMap.get(entry.getKey());
Assert.assertEquals("Property " + method.getName() + " of representation not equal.", expectedValue, actualValue); Assert.assertEquals("Map item with key '" + entry.getKey() + "' not equal.", expectedValue, actualValue);
}
}
} else {
// Reflection-baseed comparing for other types
for (Method method : Reflections.getAllDeclaredMethods(expectedRep.getClass())) {
if (method.getName().startsWith("get") || method.getName().startsWith("is")) {
Object expectedValue = Reflections.invokeMethod(method, expectedRep);
if (expectedValue != null) {
Object actualValue = Reflections.invokeMethod(method, actualRep);
Assert.assertEquals("Property method '" + method.getName() + "' of representation not equal.", expectedValue, actualValue);
}
} }
} }
} }
@ -241,5 +259,22 @@ public class AssertAdminEvents implements TestRule {
} }
} }
public static Matcher<String> isExpectedPrefixFollowedByUuid(final String prefix) {
return new TypeSafeMatcher<String>() {
@Override
protected boolean matchesSafely(String item) {
int expectedLength = prefix.length() + 1 + org.keycloak.models.utils.KeycloakModelUtils.generateId().length();
return item.startsWith(prefix) && expectedLength == item.length();
}
@Override
public void describeTo(Description description) {
description.appendText("resourcePath in the format like \"" + prefix + "/<UUID>\"");
}
};
}
} }