From 61d7b18e236a853787e56f91b602887bb30cd5e1 Mon Sep 17 00:00:00 2001
From: "Endi S. Dewata" <edewata@redhat.com>
Date: Thu, 3 Apr 2014 16:42:14 -0400
Subject: [PATCH] Added audit page.

A new page has been added to the TPS UI to manage audit configuration.
The audit REST service has been modified to use PATCH like the other
services. The audit events will now have 3 possible values: mandatory,
enabled, disabled. Mandatory events cannot be edited. Optional events
can be edited in edit mode.

Ticket #654
---
 .../com/netscape/certsrv/logging/AuditConfig.java  | 135 ++++++--------------
 .../netscape/certsrv/logging/AuditResource.java    |   5 +-
 .../com/netscape/cmstools/logging/AuditCLI.java    |  11 +-
 .../org/dogtagpki/server/rest/AuditService.java    |  82 ++++++++++---
 base/tps-tomcat/shared/webapps/tps/js/audit.js     | 136 +++++++++++++++++++++
 base/tps-tomcat/shared/webapps/tps/ui/audit.html   | 121 ++++++++++++++++++
 base/tps-tomcat/shared/webapps/tps/ui/index.html   |   7 ++
 7 files changed, 373 insertions(+), 124 deletions(-)
 create mode 100644 base/tps-tomcat/shared/webapps/tps/js/audit.js
 create mode 100644 base/tps-tomcat/shared/webapps/tps/ui/audit.html

diff --git a/base/common/src/com/netscape/certsrv/logging/AuditConfig.java b/base/common/src/com/netscape/certsrv/logging/AuditConfig.java
index 6abe7af2a8ef3053996af9b3086c8034b43ecdb7..d52dd97c63d11c00dc60bbe2d3fefdd0e1cb6f96 100644
--- a/base/common/src/com/netscape/certsrv/logging/AuditConfig.java
+++ b/base/common/src/com/netscape/certsrv/logging/AuditConfig.java
@@ -21,16 +21,15 @@ package com.netscape.certsrv.logging;
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.util.ArrayList;
-import java.util.Collection;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
-import java.util.TreeSet;
 
 import javax.xml.bind.JAXBContext;
 import javax.xml.bind.Marshaller;
 import javax.xml.bind.Unmarshaller;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
 import javax.xml.bind.annotation.XmlAttribute;
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlRootElement;
@@ -44,6 +43,7 @@ import org.jboss.resteasy.plugins.providers.atom.Link;
  * @author Endi S. Dewata
  */
 @XmlRootElement(name="Audit")
+@XmlAccessorType(XmlAccessType.NONE)
 public class AuditConfig {
 
     public static Marshaller marshaller;
@@ -63,8 +63,8 @@ public class AuditConfig {
     Boolean signed;
     Integer interval;
     Integer bufferSize;
-    Collection<String> mandatoryEvents = new TreeSet<String>();
-    Map<String, Boolean> optionalEvents = new TreeMap<String, Boolean>();
+
+    Map<String, String> eventConfigs;
 
     Link link;
 
@@ -104,107 +104,50 @@ public class AuditConfig {
         this.bufferSize = bufferSize;
     }
 
-    @XmlElement(name="MandatoryEvents")
-    @XmlJavaTypeAdapter(MandatoryEventsAdapter.class)
-    public Collection<String> getMandatoryEvents() {
-        return mandatoryEvents;
+    @XmlElement(name="Events")
+    @XmlJavaTypeAdapter(EventConfigsAdapter.class)
+    public Map<String, String> getEventConfigs() {
+        return eventConfigs;
     }
 
-    public void setMandatoryEvents(Collection<String> mandatoryEvents) {
-        this.mandatoryEvents.clear();
-        this.mandatoryEvents.addAll(mandatoryEvents);
+    public void setEventConfigs(Map<String, String> eventConfigs) {
+        this.eventConfigs = eventConfigs;
     }
 
-    public void addMandatoryEvent(String event) {
-        mandatoryEvents.add(event);
-    }
-
-    public void removeMandatoryEvent(String event) {
-        mandatoryEvents.remove(event);
-    }
-
-    @XmlElement(name="OptionalEvents")
-    @XmlJavaTypeAdapter(OptionalEventsAdapter.class)
-    public Map<String, Boolean> getOptionalEvents() {
-        return optionalEvents;
-    }
-
-    public void setOptionalEvents(Map<String, Boolean> optionalEvents) {
-        this.optionalEvents.clear();
-        this.optionalEvents.putAll(optionalEvents);
-    }
-
-    public Collection<String> getOptionalEventNames() {
-        return optionalEvents.keySet();
-    }
-
-    public Boolean getOptionalEvent(String name) {
-        return optionalEvents.get(name);
-    }
-
-    public void setOptionalEvent(String name, Boolean value) {
-        optionalEvents.put(name, value);
-    }
-
-    public Boolean removeOptionalEvent(String name) {
-        return optionalEvents.remove(name);
-    }
-
-    public static class MandatoryEventsAdapter extends XmlAdapter<EventList, Collection<String>> {
-
-        public EventList marshal(Collection<String> input) {
-            EventList output = new EventList();
-            for (String name : input) {
-                Event event = new Event();
-                event.name = name;
-                output.entries.add(event);
-            }
-            return output;
-        }
-
-        public Collection<String> unmarshal(EventList input) {
-            Collection<String> output = new TreeSet<String>();
-            for (Event event : input.entries) {
-                output.add(event.name);
-            }
-            return output;
-        }
-    }
-
-    public static class OptionalEventsAdapter extends XmlAdapter<EventList, Map<String, Boolean>> {
+    public static class EventConfigsAdapter extends XmlAdapter<EventConfigList, Map<String, String>> {
 
-        public EventList marshal(Map<String, Boolean> map) {
-            EventList list = new EventList();
-            for (Map.Entry<String, Boolean> entry : map.entrySet()) {
-                Event event = new Event();
-                event.name = entry.getKey();
-                event.value = entry.getValue();
-                list.entries.add(event);
+        public EventConfigList marshal(Map<String, String> map) {
+            EventConfigList list = new EventConfigList();
+            for (Map.Entry<String, String> entry : map.entrySet()) {
+                EventConfig eventConfig = new EventConfig();
+                eventConfig.name = entry.getKey();
+                eventConfig.value = entry.getValue();
+                list.entries.add(eventConfig);
             }
             return list;
         }
 
-        public Map<String, Boolean> unmarshal(EventList list) {
-            Map<String, Boolean> map = new LinkedHashMap<String, Boolean>();
-            for (Event event : list.entries) {
-                map.put(event.name, event.value);
+        public Map<String, String> unmarshal(EventConfigList list) {
+            Map<String, String> map = new TreeMap<String, String>();
+            for (EventConfig eventConfig : list.entries) {
+                map.put(eventConfig.name, eventConfig.value);
             }
             return map;
         }
     }
 
-    public static class EventList {
+    public static class EventConfigList {
         @XmlElement(name="Event")
-        public List<Event> entries = new ArrayList<Event>();
+        public List<EventConfig> entries = new ArrayList<EventConfig>();
     }
 
-    public static class Event {
+    public static class EventConfig {
 
         @XmlAttribute
         public String name;
 
         @XmlValue
-        public Boolean value;
+        public String value;
     }
 
     @XmlElement(name="Link")
@@ -222,10 +165,9 @@ public class AuditConfig {
         int result = 1;
         result = prime * result + ((bufferSize == null) ? 0 : bufferSize.hashCode());
         result = prime * result + ((enabled == null) ? 0 : enabled.hashCode());
-        result = prime * result + ((optionalEvents == null) ? 0 : optionalEvents.hashCode());
+        result = prime * result + ((eventConfigs == null) ? 0 : eventConfigs.hashCode());
         result = prime * result + ((interval == null) ? 0 : interval.hashCode());
         result = prime * result + ((link == null) ? 0 : link.hashCode());
-        result = prime * result + ((mandatoryEvents == null) ? 0 : mandatoryEvents.hashCode());
         result = prime * result + ((signed == null) ? 0 : signed.hashCode());
         return result;
     }
@@ -249,10 +191,10 @@ public class AuditConfig {
                 return false;
         } else if (!enabled.equals(other.enabled))
             return false;
-        if (optionalEvents == null) {
-            if (other.optionalEvents != null)
+        if (eventConfigs == null) {
+            if (other.eventConfigs != null)
                 return false;
-        } else if (!optionalEvents.equals(other.optionalEvents))
+        } else if (!eventConfigs.equals(other.eventConfigs))
             return false;
         if (interval == null) {
             if (other.interval != null)
@@ -264,11 +206,6 @@ public class AuditConfig {
                 return false;
         } else if (!link.equals(other.link))
             return false;
-        if (mandatoryEvents == null) {
-            if (other.mandatoryEvents != null)
-                return false;
-        } else if (!mandatoryEvents.equals(other.mandatoryEvents))
-            return false;
         if (signed == null) {
             if (other.signed != null)
                 return false;
@@ -304,10 +241,12 @@ public class AuditConfig {
         before.setSigned(false);
         before.setInterval(10);
         before.setBufferSize(512);
-        before.addMandatoryEvent("event1");
-        before.addMandatoryEvent("event2");
-        before.setOptionalEvent("event3", true);
-        before.setOptionalEvent("event4", false);
+
+        Map<String, String> eventConfigs = new TreeMap<String, String>();
+        eventConfigs.put("event1", "mandatory");
+        eventConfigs.put("event2", "enabled");
+        eventConfigs.put("event3", "disabled");
+        before.setEventConfigs(eventConfigs);
 
         String string = before.toString();
         System.out.println(string);
diff --git a/base/common/src/com/netscape/certsrv/logging/AuditResource.java b/base/common/src/com/netscape/certsrv/logging/AuditResource.java
index 13be4d827de24825545d35df877688972077e0de..6053c6b8fc40132f3e6281e8eb143eced332a139 100644
--- a/base/common/src/com/netscape/certsrv/logging/AuditResource.java
+++ b/base/common/src/com/netscape/certsrv/logging/AuditResource.java
@@ -18,12 +18,13 @@
 package com.netscape.certsrv.logging;
 
 import javax.ws.rs.GET;
-import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.core.Response;
 
 import org.jboss.resteasy.annotations.ClientResponseType;
 
+import com.netscape.certsrv.base.PATCH;
+
 
 /**
  * @author Endi S. Dewata
@@ -35,7 +36,7 @@ public interface AuditResource {
     @ClientResponseType(entityType=AuditConfig.class)
     public Response getAuditConfig();
 
-    @PUT
+    @PATCH
     @ClientResponseType(entityType=AuditConfig.class)
     public Response updateAuditConfig(AuditConfig configData);
 }
diff --git a/base/java-tools/src/com/netscape/cmstools/logging/AuditCLI.java b/base/java-tools/src/com/netscape/cmstools/logging/AuditCLI.java
index 42e735ecac744db6454fbf93398a9124ae595277..311a833279e2d3ef08a774a7fdfdaeff4f7b39da 100644
--- a/base/java-tools/src/com/netscape/cmstools/logging/AuditCLI.java
+++ b/base/java-tools/src/com/netscape/cmstools/logging/AuditCLI.java
@@ -57,10 +57,13 @@ public class AuditCLI extends CLI {
         if (auditConfig.getBufferSize() != null) System.out.println("  Buffer size (bytes): " + auditConfig.getBufferSize());
 
         System.out.println("  Events:");
-        Map<String, Boolean> events = auditConfig.getOptionalEvents();
-        for (String name : events.keySet()) {
-            Boolean value = events.get(name);
-            System.out.println("    " + name + ": " + value);
+        Map<String, String> eventConfigs = auditConfig.getEventConfigs();
+        if (eventConfigs != null) {
+            for (Map.Entry<String, String> entry : eventConfigs.entrySet()) {
+                String name = entry.getKey();
+                String value = entry.getValue();
+                System.out.println("    " + name + ": " + value);
+            }
         }
 
         Link link = auditConfig.getLink();
diff --git a/base/server/cms/src/org/dogtagpki/server/rest/AuditService.java b/base/server/cms/src/org/dogtagpki/server/rest/AuditService.java
index 26fd0c0d73c99682c32a3aa18f04a34d1a0e9c59..bd5625844be8e80fd5d56631a39e863c7ff29edb 100644
--- a/base/server/cms/src/org/dogtagpki/server/rest/AuditService.java
+++ b/base/server/cms/src/org/dogtagpki/server/rest/AuditService.java
@@ -21,7 +21,9 @@ package org.dogtagpki.server.rest;
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.util.Collection;
-import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.TreeSet;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.core.Context;
@@ -75,21 +77,25 @@ public class AuditService extends PKIService implements AuditResource {
         auditConfig.setInterval(cs.getInteger("log.instance.SignedAudit.flushInterval", 5));
         auditConfig.setBufferSize(cs.getInteger("log.instance.SignedAudit.bufferSize", 512));
 
-        // unselected events
+        Map<String, String> eventConfigs = new TreeMap<String, String>();
+
+        // unselected optional events
         for (String event : StringUtils.split(cs.getString("log.instance.SignedAudit.unselected.events", ""), ", ")) {
-            auditConfig.setOptionalEvent(event.trim(), false);
+            eventConfigs.put(event.trim(), "disabled");
         }
 
-        // in case of duplicates, selected events override unselected events
+        // selected optional events
         for (String event : StringUtils.split(cs.getString("log.instance.SignedAudit.events", ""), ", ")) {
-            auditConfig.setOptionalEvent(event.trim(), true);
+            eventConfigs.put(event.trim(), "enabled");
         }
 
-        // mandatory events
+        // always selected mandatory events
         for (String event : StringUtils.split(cs.getString("log.instance.SignedAudit.mandatory.events", ""), ", ")) {
-            auditConfig.addMandatoryEvent(event.trim());
+            eventConfigs.put(event.trim(), "mandatory");
         }
 
+        auditConfig.setEventConfigs(eventConfigs);
+
         URI uri = uriInfo.getBaseUriBuilder().path(AuditResource.class).build();
         auditConfig.setLink(new Link("self", uri));
 
@@ -121,6 +127,9 @@ public class AuditService extends PKIService implements AuditResource {
         CMS.debug("AuditService.updateAuditConfig()");
 
         try {
+            AuditConfig currentAuditConfig = createAuditConfig();
+            Map<String, String> currentEventConfigs = currentAuditConfig.getEventConfigs();
+
             IConfigStore cs = CMS.getConfigStore();
 
             if (auditConfig.getEnabled() != null) {
@@ -139,25 +148,58 @@ public class AuditService extends PKIService implements AuditResource {
                 cs.putInteger("log.instance.SignedAudit.bufferSize", auditConfig.getBufferSize());
             }
 
-            // update events selection
-            Collection<String> selected = new LinkedHashSet<String>();
-            Collection<String> unselected = new LinkedHashSet<String>();
+            Map<String, String> eventConfigs = auditConfig.getEventConfigs();
 
-            for (String name : auditConfig.getOptionalEvents().keySet()) {
-                Boolean value = auditConfig.getOptionalEvent(name);
+            if (eventConfigs != null) {
+                // update events if specified
 
-                if (value) {
-                    selected.add(name);
+                Collection<String> selected = new TreeSet<String>();
+                Collection<String> unselected = new TreeSet<String>();
 
-                } else {
-                    unselected.add(name);
+                for (Map.Entry<String, String> entry : eventConfigs.entrySet()) {
+                    String name = entry.getKey();
+                    String value = entry.getValue();
+                    String currentValue = currentEventConfigs.get(name);
+
+                    // make sure no event is added
+                    if (currentValue == null) {
+                        throw new PKIException("Unable to add event: " + name);
+                    }
+
+                    // make sure no optional event becomes mandatory
+                    if ("mandatory".equals(value)) {
+                        if (!"mandatory".equals(currentValue)) {
+                            throw new PKIException("Unable to add mandatory event: " + name);
+                        }
+                        continue;
+                    }
+
+                    // make sure no mandatory event becomes optional
+                    if ("mandatory".equals(currentValue)) {
+                        throw new PKIException("Unable to remove mandatory event: " + name);
+                    }
+
+                    if ("enabled".equals(value)) {
+                        selected.add(name);
+
+                    } else if ("disabled".equals(value)) {
+                        unselected.add(name);
+
+                    } else {
+                        throw new PKIException("Invalid event configuration: " + name + "=" + value);
+                    }
                 }
-            }
 
-            cs.putString("log.instance.SignedAudit.events", StringUtils.join(selected, ","));
-            cs.putString("log.instance.SignedAudit.unselected.events", StringUtils.join(unselected, ","));
+                cs.putString("log.instance.SignedAudit.events", StringUtils.join(selected, ","));
+                cs.putString("log.instance.SignedAudit.unselected.events", StringUtils.join(unselected, ","));
+            }
 
-            // mandatory events cannot be updated
+            for (String name : currentEventConfigs.keySet()) {
+                // make sure no event is removed
+                if (!eventConfigs.containsKey(name)) {
+                    throw new PKIException("Unable to remove event: " + name);
+                }
+            }
 
             cs.commit(true);
 
diff --git a/base/tps-tomcat/shared/webapps/tps/js/audit.js b/base/tps-tomcat/shared/webapps/tps/js/audit.js
new file mode 100644
index 0000000000000000000000000000000000000000..fc2bbdb1342641a70d4d0d62ba64d96c9677d1d9
--- /dev/null
+++ b/base/tps-tomcat/shared/webapps/tps/js/audit.js
@@ -0,0 +1,136 @@
+/* --- BEGIN COPYRIGHT BLOCK ---
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ * All rights reserved.
+ * --- END COPYRIGHT BLOCK ---
+ *
+ * @author Endi S. Dewata
+ */
+
+var AuditModel = Model.extend({
+    url: function() {
+        return "/tps/rest/audit";
+    },
+    parseResponse: function(response) {
+        return {
+            id: "audit",
+            enabled: response.Enabled,
+            signed: response.Signed,
+            interval: response.Interval,
+            bufferSize: response.BufferSize,
+            events: response.Events.Event
+        };
+    },
+    createRequest: function(entry) {
+        return {
+            Enabled: entry.enabled,
+            Signed: entry.signed,
+            Interval: entry.interval,
+            BufferSize: entry.bufferSize,
+            Events: {
+                Event: entry.events
+            }
+        };
+    }
+});
+
+var AuditTableItem = TableItem.extend({
+    initialize: function(options) {
+        var self = this;
+        AuditTableItem.__super__.initialize.call(self, options);
+    },
+    renderIDColumn: function(td) {
+        var self = this;
+
+        // in view mode all events are read-only
+        if (self.table.mode == "view") {
+            self.renderColumn(td);
+            return;
+        }
+
+        // mandatory events are read-only
+        var value = self.get("value");
+        if (value == "mandatory") {
+            self.renderColumn(td);
+            return;
+        }
+
+        // optional events are editable in edit mode
+        AuditTableItem.__super__.renderIDColumn.call(self, td);
+    }
+});
+
+var AuditPage = EntryPage.extend({
+    initialize: function(options) {
+        var self = this;
+        options.model = new AuditModel();
+        options.editable = ["enabled", "signed", "interval", "bufferSize"];
+        AuditPage.__super__.initialize.call(self, options);
+    },
+    setup: function() {
+        var self = this;
+
+        AuditPage.__super__.setup.call(self);
+
+        var eventDialog = self.$("#event-dialog");
+
+        var eventEditDialog = new Dialog({
+            el: eventDialog,
+            title: "Edit Event",
+            readonly: ["name"],
+            actions: ["cancel", "save"]
+        });
+
+        var eventViewDialog = new Dialog({
+            el: eventDialog,
+            title: "Event",
+            readonly: ["name", "value"],
+            actions: ["close"]
+        });
+
+        self.eventsTable = new Table({
+            el: self.$("table[name='events']"),
+            columnMappings: {
+                id: "name"
+            },
+            editDialog: eventEditDialog,
+            viewDialog: eventViewDialog,
+            pageSize: 10,
+            tableItem: AuditTableItem
+        });
+    },
+    renderContent: function() {
+        var self = this;
+
+        AuditPage.__super__.renderContent.call(self);
+
+        if (self.mode == "edit") {
+            self.eventsTable.mode = "edit";
+
+        } else { // self.mode == "view"
+            self.eventsTable.mode = "view";
+        }
+
+        self.eventsTable.entries = self.attributes.events;
+        self.eventsTable.render();
+    },
+    saveFields: function() {
+        var self = this;
+
+        AuditPage.__super__.saveFields.call(self);
+
+        self.attributes.events = self.eventsTable.entries;
+    }
+});
diff --git a/base/tps-tomcat/shared/webapps/tps/ui/audit.html b/base/tps-tomcat/shared/webapps/tps/ui/audit.html
new file mode 100644
index 0000000000000000000000000000000000000000..d74a7b5a76732e9166a1ac1476c2d7a5433045c9
--- /dev/null
+++ b/base/tps-tomcat/shared/webapps/tps/ui/audit.html
@@ -0,0 +1,121 @@
+<!-- --- BEGIN COPYRIGHT BLOCK ---
+     This program is free software; you can redistribute it and/or modify
+     it under the terms of the GNU General Public License as published by
+     the Free Software Foundation; version 2 of the License.
+
+     This program is distributed in the hope that it will be useful,
+     but WITHOUT ANY WARRANTY; without even the implied warranty of
+     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+     GNU General Public License for more details.
+
+     You should have received a copy of the GNU General Public License along
+     with this program; if not, write to the Free Software Foundation, Inc.,
+     51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+     Copyright (C) 2014 Red Hat, Inc.
+     All rights reserved.
+     --- END COPYRIGHT BLOCK --- -->
+<div class="pki-header">
+
+<span class="pki-title">Audit</span>
+
+<span class="pki-actions">
+
+<span class="pki-menu" style="display: none;">
+<a name="edit" href="#">Edit</a><br>
+</span>
+
+<span class="pki-buttons" style="display: none;">
+<button name="cancel">Cancel</button>
+<button name="save" class="primary">Save</button>
+</span>
+
+</span>
+
+</div>
+
+<div name="audit" class="pki-fields">
+<fieldset>
+    <label>Enabled</label>
+    <input name="enabled" readonly="readonly">
+    <br>
+    <label>Signed</label>
+    <input name="signed" readonly="readonly">
+    <br>
+    <label>Interval</label>
+    <input name="interval" readonly="readonly">
+    <br>
+    <label>Buffer Size</label>
+    <input name="bufferSize" readonly="readonly">
+    <br>
+</fieldset>
+</div>
+
+<h2>Events</h2>
+
+<table name="events">
+<thead>
+    <tr>
+         <th class="pki-table-actions" colspan="3">
+             <span name="search">
+                 <input name="search" type="text" placeholder="Search...">
+             </span>
+             <span class="pki-table-buttons" style="display: none;">
+             </span>
+         </th>
+    </tr>
+    <tr>
+        <th class="pki-select-column"><input id="events_selectall" type="checkbox"><label for="events_selectall">&nbsp;</label></th>
+        <th class="pki-property-name-column">Name</th>
+        <th>Value</th>
+    </tr>
+</thead>
+<tbody>
+    <tr>
+        <td class="pki-select-column"><input id="events_select" type="checkbox"><label for="events_select">&nbsp;</label></td>
+        <td name="id" class="pki-property-name-column"><a href="#">&nbsp;</a></td>
+        <td name="value">&nbsp;</td>
+    </tr>
+</tbody>
+<tfoot>
+    <tr>
+         <th class="pki-table-actions" colspan="3">
+             <div class="pki-table-info">
+                 Total: <span name="totalEntries">0</span> entries
+             </div>
+             <div class="pki-page-controls">
+                 <ul class="pagination">
+                     <li><a href="#" name="first"><span class="i fa fa-angle-double-left"></span></a></li>
+                     <li><a href="#" name="prev"><span class="i fa fa-angle-left"></span></a></li>
+                 </ul>
+                 <span class="pki-page-jump">
+                     <input name="page" type="text" value="1"> of <span name="totalPages">1</span>
+                 </span>
+                 <ul class="pagination">
+                     <li><a href="#" name="next"><span class="i fa fa-angle-right"></span></a></li>
+                     <li><a href="#" name="last"><span class="i fa fa-angle-double-right"></span></a></li>
+                 </ul>
+             </div>
+         </th>
+    </tr>
+</tfoot>
+</table>
+
+<div id="event-dialog" class="rcue-dialog-background">
+    <div class="rcue-dialog">
+        <header>
+            <h1>Edit Event</h1>
+            <a class="rcue-button-close" href="#"></a>
+        </header>
+        <fieldset>
+            <label>Name</label><input name="name" type="text"><br>
+            <label>Value</label><input name="value" type="text"><br>
+        </fieldset>
+        <footer>
+            <button name="add" class="primary">Add</button>
+            <button name="save" class="primary">Save</button>
+            <button name="close" class="primary">Close</button>
+            <button name="cancel">Cancel</button>
+        </footer>
+    </div>
+</div>
diff --git a/base/tps-tomcat/shared/webapps/tps/ui/index.html b/base/tps-tomcat/shared/webapps/tps/ui/index.html
index 1eda85dad6830861dc850119b42477c939bb50ea..fd950cad5ee9030d7d3c9da873d843b9eaf6e821 100644
--- a/base/tps-tomcat/shared/webapps/tps/ui/index.html
+++ b/base/tps-tomcat/shared/webapps/tps/ui/index.html
@@ -28,6 +28,7 @@
     <script src="/tps/js/tps.js"></script>
     <script src="/tps/js/account.js"></script>
     <script src="/tps/js/activity.js"></script>
+    <script src="/tps/js/audit.js"></script>
     <script src="/tps/js/authenticator.js"></script>
     <script src="/tps/js/cert.js"></script>
     <script src="/tps/js/connection.js"></script>
@@ -64,6 +65,11 @@ $(function() {
         url: "activities.html"
     }));
 
+    navigation.page("audit", new AuditPage({
+        el: content,
+        url: "audit.html"
+    }));
+
     navigation.page("authenticators", new AuthenticatorsPage({
         el: content,
         url: "authenticators.html"
@@ -175,6 +181,7 @@ $(function() {
       <ul class="dropdown-menu">
         <li><a href="#authenticators">Authenticators</a></li>
         <li><a href="#connections">Connections</a></li>
+        <li><a href="#audit">Audit</a></li>
       </ul>
     </li>
     </ul>
-- 
1.8.4.2

