package jnpf.elnService.template; import jnpf.elnEntity.template.ElnTemplateVersionEntity; import jnpf.util.JsonUtil; import org.junit.Test; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class TemplateAnnotationAuthorizerTest { @Test public void successfulAuthorizationKeepsTenantUntilOuterExecutionCompletes() { List events = new ArrayList<>(); RecordingTenantRoute tenants = new RecordingTenantRoute(events); TemplateAnnotationAuthorizer authorizer = authorizer(validPayload("tenant-a", "template-1", "file-1"), template("tenant-a", "template-1", "file-1", "DRAFT", "enabled"), true, tenants, events); TemplateAnnotationAuthorization result = authorizer.authorize("ticket-1", "template-1"); assertEquals(Arrays.asList("access:tenant-a:user-1:template-1:file-1", "switch:tenant-a", "lookup:tenant-a:template-1"), events); assertEquals("tenant-a", tenants.currentTenant); assertEquals("tenant-a", result.getTenantId()); assertEquals("user-1", result.getUserId()); assertEquals("template-1", result.getTemplateId()); assertEquals("file-1", result.getFileId()); for (Field field : TemplateAnnotationAuthorization.class.getDeclaredFields()) { if (!field.isSynthetic()) { assertTrue(Modifier.isFinal(field.getModifiers())); } } events.add("service:" + tenants.currentTenant); tenants.clear(); assertEquals(Arrays.asList("access:tenant-a:user-1:template-1:file-1", "switch:tenant-a", "lookup:tenant-a:template-1", "service:tenant-a", "clear"), events); assertNull(tenants.currentTenant); } @Test public void requestCannotOverrideSignedTenantAndSequentialTenantsDoNotLeak() { List events = new ArrayList<>(); RecordingTenantRoute tenants = new RecordingTenantRoute(events); AtomicReference routedTenant = new AtomicReference<>(); TemplateAnnotationTicketService tickets = new TemplateAnnotationTicketService(ticket -> JsonUtil.getObjectToString(validPayload(ticket, "template-1", "file-1"))); TemplateAnnotationAuthorizer authorizer = new TemplateAnnotationAuthorizer(tickets, templateId -> { routedTenant.set(tenants.currentTenant); return template(tenants.currentTenant, templateId, "file-1", "DRAFT", "enabled"); }, (tenantId, userId, templateId, fileId) -> true, tenants); authorizer.authorize("tenant-a", "template-1"); assertEquals("tenant-a", routedTenant.get()); assertEquals("tenant-a", tenants.currentTenant); tenants.clear(); assertNull(tenants.currentTenant); authorizer.authorize("tenant-b", "template-1"); assertEquals("tenant-b", routedTenant.get()); assertEquals("tenant-b", tenants.currentTenant); tenants.clear(); assertNull(tenants.currentTenant); assertEquals(Arrays.asList("tenant-a", "tenant-b"), tenants.switchedTenants); assertEquals(2, tenants.clearCount); } @Test public void tenantSwitchFailureDoesNotQueryTemplateAndStillClears() { List events = new ArrayList<>(); RecordingTenantRoute tenants = new RecordingTenantRoute(events); tenants.failSwitch = true; AtomicInteger queries = new AtomicInteger(); TemplateAnnotationAuthorizer authorizer = new TemplateAnnotationAuthorizer(ticketService(validPayload()), templateId -> { queries.incrementAndGet(); return template("tenant-a", templateId, "file-1", "DRAFT", "enabled"); }, (tenantId, userId, templateId, fileId) -> true, tenants); assertStatus(401, () -> authorizer.authorize("ticket-1", "template-1")); assertEquals(0, queries.get()); assertEquals(Arrays.asList("switch:tenant-a", "clear"), events); assertNull(tenants.currentTenant); } @Test public void templateAndFileMustMatchSignedTicket() { RecordingTenantRoute tenants = new RecordingTenantRoute(new ArrayList<>()); TemplateAnnotationAuthorizer authorizer = authorizer(validPayload(), template("tenant-a", "template-1", "other-file", "DRAFT", "enabled"), true, tenants, new ArrayList<>()); assertStatus(401, () -> authorizer.authorize("ticket-1", "template-2")); assertStatus(401, () -> authorizer.authorize("ticket-1", "template-1")); assertNull(tenants.currentTenant); } @Test public void attachmentMustContainExactlyOneDocx() { RecordingTenantRoute tenants = new RecordingTenantRoute(new ArrayList<>()); ElnTemplateVersionEntity template = template("tenant-a", "template-1", "file-1", "DRAFT", "enabled"); template.setMubanFile("[{\"fileId\":\"file-1\",\"fileExtension\":\"docx\"}," + "{\"fileId\":\"file-2\",\"fileExtension\":\"DOCX\"}]"); TemplateAnnotationAuthorizer authorizer = authorizer(validPayload(), template, true, tenants, new ArrayList<>()); assertStatus(409, () -> authorizer.authorize("ticket-1", "template-1")); assertNull(tenants.currentTenant); } @Test public void templateMustRemainDraftAndEnabled() { RecordingTenantRoute tenants = new RecordingTenantRoute(new ArrayList<>()); assertStatus(409, () -> authorizer(validPayload(), template("tenant-a", "template-1", "file-1", "PUBLISHED", "enabled"), true, tenants, new ArrayList<>()).authorize("ticket-1", "template-1")); assertStatus(409, () -> authorizer(validPayload(), template("tenant-a", "template-1", "file-1", "DRAFT", "disabled"), true, tenants, new ArrayList<>()).authorize("ticket-1", "template-1")); assertNull(tenants.currentTenant); } @Test public void deletedTemplateCannotBeAnnotated() { RecordingTenantRoute tenants = new RecordingTenantRoute(new ArrayList<>()); ElnTemplateVersionEntity template = template("tenant-a", "template-1", "file-1", "DRAFT", "enabled"); template.setFDeleteMark(1); assertStatus(409, () -> authorizer(validPayload(), template, true, tenants, new ArrayList<>()).authorize("ticket-1", "template-1")); assertNull(tenants.currentTenant); } @Test public void revokedAnnotationAccessIsForbidden() { RecordingTenantRoute tenants = new RecordingTenantRoute(new ArrayList<>()); ElnTemplateVersionEntity template = template("tenant-a", "template-1", "file-1", "DRAFT", "enabled"); assertStatus(403, () -> authorizer(validPayload(), template, false, tenants, new ArrayList<>()).authorize("ticket-1", "template-1")); assertNull(tenants.currentTenant); } @Test public void malformedTicketAlsoClearsExistingTenantContext() { List events = new ArrayList<>(); RecordingTenantRoute tenants = new RecordingTenantRoute(events); tenants.currentTenant = "stale-tenant"; TemplateAnnotationAuthorizer authorizer = new TemplateAnnotationAuthorizer( new TemplateAnnotationTicketService(ticket -> null), templateId -> null, (tenantId, userId, templateId, fileId) -> true, tenants); assertStatus(401, () -> authorizer.authorize("expired", "template-1")); assertNull(tenants.currentTenant); assertEquals(Arrays.asList("clear"), events); } private TemplateAnnotationAuthorizer authorizer(Map payload, ElnTemplateVersionEntity template, boolean allowed, RecordingTenantRoute tenants, List events) { return new TemplateAnnotationAuthorizer(ticketService(payload), templateId -> { events.add("lookup:" + tenants.currentTenant + ":" + templateId); return template; }, (tenantId, userId, templateId, fileId) -> { events.add("access:" + tenantId + ":" + userId + ":" + templateId + ":" + fileId); return allowed; }, tenants); } private TemplateAnnotationTicketService ticketService(Map payload) { return new TemplateAnnotationTicketService(ticket -> JsonUtil.getObjectToString(payload)); } private ElnTemplateVersionEntity template(String tenantId, String templateId, String fileId, String status, String enabled) { ElnTemplateVersionEntity template = new ElnTemplateVersionEntity(); template.setFTenantId(tenantId); template.setId(templateId); template.setMubanFile("[{\"fileId\":\"" + fileId + "\",\"fileExtension\":\"docx\"}]"); template.setBizStatus(status); template.setBizEnabled(enabled); return template; } private Map validPayload() { return validPayload("tenant-a", "template-1", "file-1"); } private Map validPayload(String tenantId, String templateId, String fileId) { Map payload = new LinkedHashMap<>(); payload.put("scope", "onlyoffice:eln-template-annotate"); payload.put("tenantId", tenantId); payload.put("userId", "user-1"); payload.put("fileId", fileId); payload.put("bizDataId", templateId); payload.put("bizModule", "eln.template"); payload.put("bizScene", "eln.template.annotate"); payload.put("pluginGuid", "asc.{2D6D6CB6-F6FC-45B7-8EC7-0CC1940F1A6F}"); return payload; } private void assertStatus(int expected, ThrowingRunnable operation) { try { operation.run(); fail("expected authorization failure"); } catch (TemplateAnnotationTicketService.InvalidAnnotationTicketException e) { assertEquals(expected, e.getStatus()); } catch (TemplateAnnotationAuthorizer.AnnotationAuthorizationException e) { assertEquals(expected, e.getStatus()); assertFalse(e.getMessage().contains("module-template")); assertFalse(e.getMessage().contains("tenant-a")); } } private interface ThrowingRunnable { void run(); } private static class RecordingTenantRoute implements TemplateAnnotationAuthorizer.TenantRoute { private final List events; private final List switchedTenants = new ArrayList<>(); private String currentTenant; private boolean failSwitch; private int clearCount; private RecordingTenantRoute(List events) { this.events = events; } @Override public void switchTo(String tenantId) { events.add("switch:" + tenantId); switchedTenants.add(tenantId); if (failSwitch) { throw new IllegalStateException("switch failed"); } currentTenant = tenantId; } @Override public void clear() { events.add("clear"); clearCount++; currentTenant = null; } } }