diff --git a/warden3/warden_client/warden_client.py b/warden3/warden_client/warden_client.py
index e2ed6bb80972f5a57335fba4971d54c8040d0605..25e9e73a13680f3bad100d7778edae6cb6f37f45 100644
--- a/warden3/warden_client/warden_client.py
+++ b/warden3/warden_client/warden_client.py
@@ -211,7 +211,7 @@ class Client(object):
                 Error("Unable to setup syslog logging", self.logger, exc=exc_info())
 
         if not (errlog or filelog or syslog):
-            # User wants explicitly no logging, so let him shoot its socks off.
+            # User wants explicitly no logging, so let him shoot his socks off.
             # This silences complaining of logging module about no suitable
             # handler.
             self.logger.addHandler(logging.NullHandler())
@@ -252,6 +252,8 @@ class Client(object):
 
     def sendRequest(self, func="", payload=None, **kwargs):
 
+        kwargs["client"] = self.name
+
         if kwargs:
             for k in kwargs.keys():
                 if kwargs[k] is None:
diff --git a/warden3/warden_client/warden_curl_test.sh b/warden3/warden_client/warden_curl_test.sh
index 95af2e514ca1f58ac8f206dab761d8fe13c06185..bab94acacd3b05bc47f8a584b25b56ba51034594 100755
--- a/warden3/warden_client/warden_curl_test.sh
+++ b/warden3/warden_client/warden_curl_test.sh
@@ -6,7 +6,8 @@
 keyfile='key.pem'
 certfile='cert.pem'
 cafile='tcs-ca-bundle.pem'
-url='https://warden.example.com/warden3'
+url="$1"
+client="$2"
 
 #    --fail \
 #    --show-error \
@@ -19,7 +20,7 @@ curl \
     --cacert $cafile \
     --connect-timeout 3 \
     --request POST \
-    $url/blefub
+    "$url/blefub?client=$2"
 echo
 
 echo "Test  404"
@@ -29,7 +30,27 @@ curl \
     --cacert $cafile \
     --connect-timeout 3 \
     --request POST \
-    $url/
+    "$url/?client=$2"
+echo
+
+echo "Test  403 - no client"
+curl \
+    --key $keyfile \
+    --cert $certfile \
+    --cacert $cafile \
+    --connect-timeout 3 \
+    --request POST \
+    "$url/getEvents"
+echo
+
+echo "Test  403 - wrong client"
+curl \
+    --key $keyfile \
+    --cert $certfile \
+    --cacert $cafile \
+    --connect-timeout 3 \
+    --request POST \
+    "$url/getEvents?client=asdf.blefub"
 echo
 
 echo "Test  Deserialization"
@@ -40,7 +61,7 @@ curl \
     --connect-timeout 3 \
     --request POST \
     --data '{#$%^' \
-    $url/getEvents
+    "$url/getEvents?client=$2"
 echo
 
 echo "Test  Called with unknown category"
@@ -50,7 +71,7 @@ curl \
     --cacert $cafile \
     --connect-timeout 3 \
     --request POST \
-    $url/getEvents?cat=bflm
+    "$url/getEvents?client=$2&cat=bflm"
 echo
 
 echo "Test  Called with both cat and nocat"
@@ -60,7 +81,7 @@ curl \
     --cacert $cafile \
     --connect-timeout 3 \
     --request POST \
-    "$url/getEvents?cat=Other&nocat=Test"
+    "$url/getEvents?client=$2&cat=Other&nocat=Test"
 echo
 
 echo "Test  Invalid data for getEvents - silently discarded"
@@ -71,7 +92,7 @@ curl \
     --connect-timeout 3 \
     --request POST \
     --data '[1]' \
-    $url/getEvents
+    "$url/getEvents?client=$2"
 echo
 
 echo "Test  Called with internal args - just in log"
@@ -81,7 +102,7 @@ curl \
     --cacert $cafile \
     --connect-timeout 3 \
     --request POST \
-    $url/getEvents?self=test
+    "$url/getEvents?client=$2&self=test"
 echo
 
 echo "Test  Called with superfluous args - just in log"
@@ -91,7 +112,7 @@ curl \
     --cacert $cafile \
     --connect-timeout 3 \
     --request POST \
-    $url/getEvents?bad=guy
+    "$url/getEvents?client=$2&bad=guy"
 echo
 
 echo "Test  getEvents with no args - should be OK"
@@ -101,7 +122,7 @@ curl \
     --cacert $cafile \
     --connect-timeout 3 \
     --request POST \
-    $url/getEvents
+    "$url/getEvents?client=$2"
 echo
 
 echo "Test  getEvents - should be OK"
@@ -111,7 +132,7 @@ curl \
     --cacert $cafile \
     --connect-timeout 3 \
     --request POST \
-    "$url/getEvents?count=3&id=10"
+    "$url/getEvents?client=$2&count=3&id=10"
 echo
 
 echo "Test  getDebug"
@@ -121,7 +142,7 @@ curl \
     --cacert $cafile \
     --connect-timeout 3 \
     --request POST \
-    $url/getDebug
+    "$url/getDebug?client=$2"
 echo
 
 echo "Test  getInfo"
@@ -131,7 +152,7 @@ curl \
     --cacert $cafile \
     --connect-timeout 3 \
     --request POST \
-    $url/getInfo
+    "$url/getInfo?client=$2"
 echo
 
 #curl \
diff --git a/warden3/warden_server/warden3.0-alpha.sql b/warden3/warden_server/warden3.0-alpha.sql
index 14ed3ea7fbcf0828d65c76566392147e0206d7d4..0f66f01553aef3c97c64c076341ff37d9a84455c 100644
--- a/warden3/warden_server/warden3.0-alpha.sql
+++ b/warden3/warden_server/warden3.0-alpha.sql
@@ -32,7 +32,7 @@ CREATE TABLE IF NOT EXISTS `categories` (
   `subcategory` varchar(64) DEFAULT NULL,
   `cat_subcat` varchar(129) NOT NULL,
   KEY `cat_sub` (`cat_subcat`)
-) ENGINE=MyISAM DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
 
 --
 -- Dumping data for table `categories`
@@ -90,37 +90,56 @@ INSERT INTO `categories` (`id`, `category`, `subcategory`, `cat_subcat`) VALUES
 
 CREATE TABLE IF NOT EXISTS `clients` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
-  `hostname` varchar(256) NOT NULL,
   `registered` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
   `requestor` varchar(256) NOT NULL,
+  `hostname` varchar(256) NOT NULL,
+  `service` varchar(256) NOT NULL,
   `note` text NOT NULL,
   `valid` tinyint(1) NOT NULL DEFAULT '1',
-  `read` tinyint(1) NOT NULL DEFAULT '0',
+  `identity` varchar(64) NOT NULL,
+  `read` tinyint(1) NOT NULL DEFAULT '1',
   `debug` tinyint(1) NOT NULL DEFAULT '0',
+  `write` tinyint(1) NOT NULL DEFAULT '0',
+  `test` int(11) NOT NULL DEFAULT '0',
   PRIMARY KEY (`id`)
-) ENGINE=MyISAM  DEFAULT CHARSET=latin1 AUTO_INCREMENT=29 ;
+) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci AUTO_INCREMENT=31 ;
 
 --
 -- Dumping data for table `clients`
 --
 
-INSERT INTO `clients` (`id`, `hostname`, `registered`, `requestor`, `note`, `valid`, `read`, `debug`) VALUES
-(1, 'afrodita.civ.zcu.cz', '0000-00-00 00:00:00', '', '', 1, 1, 1),
-(3, 'au1.cesnet.cz', '0000-00-00 00:00:00', '', '', 1, 1, 0),
-(12, 'au2.cesnet.cz', '0000-00-00 00:00:00', '', '', 1, 1, 0),
-(13, 'bee.net.vutbr.cz', '0000-00-00 00:00:00', '', '', 1, 1, 0),
-(14, 'buldog.vsb.cz', '0000-00-00 00:00:00', '', '', 1, 1, 0),
-(15, 'collector-nemea.liberouter.org', '0000-00-00 00:00:00', '', '', 1, 1, 0),
-(17, 'collector.liberouter.org', '0000-00-00 00:00:00', '', '', 1, 1, 0),
-(18, 'holly.cesnet.cz', '0000-00-00 00:00:00', '', '', 1, 1, 0),
-(19, 'kryten.cesnet.cz', '0000-00-00 00:00:00', '', '', 1, 1, 0),
-(20, 'mentat.cesnet.cz', '0000-00-00 00:00:00', '', '', 1, 1, 0),
-(21, 'miel.opf.slu.cz', '0000-00-00 00:00:00', '', '', 1, 1, 0),
-(23, 'nfsen.ics.muni.cz', '0000-00-00 00:00:00', '', '', 1, 1, 0),
-(25, 'vinovago.cesnet.cz', '0000-00-00 00:00:00', '', '', 1, 1, 0),
-(26, 'ward.tul.cz', '0000-00-00 00:00:00', '', '', 1, 1, 0),
-(28, 'kostik.zcu.cz', '0000-00-00 00:00:00', 'kostenec@civ.zcu.cz', '', 1, 1, 1),
-(29, 'grey.cesnet.cz', '2014-12-11 13:51:14', 'ph@cesnet.cz', '', 1, 1, 1);
+INSERT INTO `clients` (`id`, `registered`, `requestor`, `hostname`, `service`, `note`, `valid`, `identity`, `read`, `debug`, `write`, `test`) VALUES
+(1, '0000-00-00 00:00:00', '', 'afrodita.civ.zcu.cz', 'hihat', '', 1, 'cz.zcu.civ.afrodita.hihat', 1, 0, 1, 0),
+(2, '0000-00-00 00:00:00', '', 'afrodita.civ.zcu.cz', 'labrea', '', 1, 'cz.zcu.civ.afrodita.labrea', 1, 0, 1, 0),
+(3, '0000-00-00 00:00:00', '', 'au1.cesnet.cz', 'cesnet_ids', '', 1, 'cz.cesnet.au1.cesnet_ids', 1, 0, 1, 0),
+(4, '0000-00-00 00:00:00', '', 'au1.cesnet.cz', 'cesnet_sserv', '', 1, 'cz.cesnet.au1.cesnet_sserv', 1, 0, 1, 0),
+(5, '0000-00-00 00:00:00', '', 'au1.cesnet.cz', 'n6d-dorkbot', '', 1, 'cz.cesnet.au1.n6d-dorkbot', 1, 0, 1, 0),
+(6, '0000-00-00 00:00:00', '', 'au1.cesnet.cz', 'n6e-certplsinkhole', '', 1, 'cz.cesnet.au1.n6e-certplsinkhole', 1, 0, 1, 0),
+(7, '0000-00-00 00:00:00', '', 'au1.cesnet.cz', 'n6i-citadelsinkhole', '', 1, 'cz.cesnet.au1.n6i-citadelsinkhole', 1, 0, 1, 0),
+(8, '0000-00-00 00:00:00', '', 'au1.cesnet.cz', 'n6i-citadelsinkholeqd', '', 1, 'cz.cesnet.au1.n6i-citadelsinkholeqd', 1, 0, 1, 0),
+(9, '0000-00-00 00:00:00', '', 'au1.cesnet.cz', 'n6n-openntp', '', 1, 'cz.cesnet.au1.n6n-openntp', 1, 0, 1, 0),
+(10, '0000-00-00 00:00:00', '', 'au1.cesnet.cz', 'n6o-botszeroaccess', '', 1, 'cz.cesnet.au1.n6o-botszeroaccess', 1, 0, 1, 0),
+(11, '0000-00-00 00:00:00', '', 'au1.cesnet.cz', 'report_n6v-virut', '', 1, 'cz.cesnet.au1.report_n6v-virut', 1, 0, 1, 0),
+(12, '0000-00-00 00:00:00', '', 'au2.cesnet.cz', 'ids-cz', '', 1, 'cz.cesnet.au2.ids-cz', 1, 0, 1, 0),
+(13, '0000-00-00 00:00:00', '', 'bee.net.vutbr.cz', 'hpscan', '', 1, 'cz.vutbr.net.bee.hpscan', 1, 0, 1, 0),
+(14, '0000-00-00 00:00:00', '', 'buldog.vsb.cz', 'kippo', '', 1, 'cz.vsb.buldog.kippo', 1, 0, 1, 0),
+(15, '0000-00-00 00:00:00', '', 'collector-nemea.liberouter.org', 'nemea', '', 1, 'org.liberouter.collector-nemea.nemea', 1, 0, 1, 0),
+(16, '0000-00-00 00:00:00', '', 'collector.liberouter.org', 'hoststats', '', 1, 'org.liberouter.collector.hoststats', 1, 0, 1, 0),
+(17, '0000-00-00 00:00:00', '', 'collector.liberouter.org', 'synscandetector_1_0', '', 1, 'org.liberouter.collector.synscandetector_1_0', 1, 0, 1, 0),
+(18, '0000-00-00 00:00:00', '', 'holly.cesnet.cz', 'kippohoneypot', '', 1, 'cz.cesnet.holly.kippohoneypot', 1, 0, 1, 0),
+(19, '0000-00-00 00:00:00', '', 'kryten.cesnet.cz', 'dionaeahoneypot', '', 1, 'cz.cesnet.kryten.dionaeahoneypot', 1, 0, 1, 0),
+(20, '0000-00-00 00:00:00', '', 'mentat.cesnet.cz', 'mentat', '', 1, 'cz.cesnet.mentat.mentat', 1, 0, 1, 0),
+(21, '0000-00-00 00:00:00', '', 'miel.opf.slu.cz', 'kippo', '', 1, 'cz.slu.opf.miel.kippo', 1, 0, 1, 0),
+(22, '0000-00-00 00:00:00', '', 'nfsen.ics.muni.cz', 'honeyscan', '', 1, 'cz.muni.ics.nfsen.honeyscan', 1, 0, 1, 0),
+(23, '0000-00-00 00:00:00', '', 'nfsen.ics.muni.cz', 'scandetector_1_0', '', 1, 'cz.muni.ics.nfsen.scandetector_1_0', 1, 0, 1, 0),
+(24, '0000-00-00 00:00:00', '', 'nfsen.ics.muni.cz', 'sshbruteforce-1_n', '', 1, 'cz.muni.ics.nfsen.sshbruteforce-1_n', 1, 0, 1, 0),
+(25, '0000-00-00 00:00:00', '', 'vinovago.cesnet.cz', 'fail2ban', '', 1, 'cz.cesnet.vinovago.fail2ban', 1, 0, 1, 0),
+(26, '0000-00-00 00:00:00', '', 'ward.tul.cz', 'dionaeatul', '', 1, 'cz.tul.ward.dionaeatul', 1, 0, 1, 0),
+(27, '0000-00-00 00:00:00', '', 'ward.tul.cz', 'kippo', '', 1, 'cz.tul.ward.kippo', 1, 0, 1, 0),
+(28, '0000-00-00 00:00:00', 'kostenec@civ.zcu.cz', 'kostik.zcu.cz', 'com.example.test-node', '', 1, 'com.example.test-node', 1, 0, 0, 0),
+(29, '0000-00-00 00:00:00', 'kostenec@civ.zcu.cz', 'kostik.zcu.cz', 'com.example.test-node2', '', 1, 'com.example.test-node2', 1, 0, 0, 0),
+(30, '0000-00-00 00:00:00', 'kostenec@civ.zcu.cz', 'kostik.zcu.cz', 'Test', '', 1, 'com.example.test-node3', 1, 0, 0, 0),
+(31, '2014-12-11 13:51:18', 'ph@cesnet.cz', 'grey.cesnet.cz', 'Test', '', 1, 'cz.cesnet.grey.test', 1, 1, 1, 0);
 
 -- --------------------------------------------------------
 
@@ -131,13 +150,12 @@ INSERT INTO `clients` (`id`, `hostname`, `registered`, `requestor`, `note`, `val
 CREATE TABLE IF NOT EXISTS `events` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `received` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
-  `service_id` int(11) NOT NULL,
+  `client_id` int(11) NOT NULL,
   `data` text NOT NULL,
   `valid` tinyint(1) NOT NULL DEFAULT '1',
   PRIMARY KEY (`id`),
-  KEY `detected` (`detected`),
-  KEY `id` (`id`,`service_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;
+  KEY `id` (`id`,`client_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4  DEFAULT COLLATE utf8mb4_unicode_ci AUTO_INCREMENT=1 ;
 
 -- --------------------------------------------------------
 
@@ -149,7 +167,7 @@ CREATE TABLE IF NOT EXISTS `event_category_mapping` (
   `event_id` int(11) NOT NULL,
   `category_id` int(11) NOT NULL,
   KEY `event_id_2` (`event_id`,`category_id`)
-) ENGINE=MyISAM DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
 
 -- --------------------------------------------------------
 
@@ -161,7 +179,7 @@ CREATE TABLE IF NOT EXISTS `event_tag_mapping` (
   `event_id` int(11) NOT NULL,
   `tag_id` int(11) NOT NULL,
   KEY `event_id_2` (`event_id`,`tag_id`)
-) ENGINE=MyISAM DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
 
 -- --------------------------------------------------------
 
@@ -176,64 +194,7 @@ CREATE TABLE IF NOT EXISTS `last_events` (
   `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
   PRIMARY KEY (`id`),
   KEY `client_id` (`client_id`,`event_id`)
-) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;
-
--- --------------------------------------------------------
-
---
--- Table structure for table `services`
---
-
-CREATE TABLE IF NOT EXISTS `services` (
-  `service_id` int(11) NOT NULL AUTO_INCREMENT,
-  `client_id` int(11) NOT NULL,
-  `registered` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
-  `requestor` varchar(256) NOT NULL,
-  `service` varchar(256) NOT NULL,
-  `note` text NOT NULL,
-  `valid` tinyint(1) NOT NULL DEFAULT '1',
-  `identity` varchar(64) NOT NULL,
-  `write` tinyint(1) NOT NULL DEFAULT '0',
-  `test` int(11) NOT NULL DEFAULT '0',
-  PRIMARY KEY (`service_id`)
-) ENGINE=MyISAM  DEFAULT CHARSET=latin1 AUTO_INCREMENT=31 ;
-
---
--- Dumping data for table `services`
---
-
-INSERT INTO `services` (`service_id`, `client_id`, `registered`, `requestor`, `service`, `note`, `valid`, `identity`, `write`, `test`) VALUES
-(1, 1, '0000-00-00 00:00:00', '', 'hihat', '', 1, 'cz.zcu.civ.afrodita.hihat', 1, 0),
-(2, 1, '0000-00-00 00:00:00', '', 'labrea', '', 1, 'cz.zcu.civ.afrodita.labrea', 1, 0),
-(3, 3, '0000-00-00 00:00:00', '', 'cesnet_ids', '', 1, 'cz.cesnet.au1.cesnet_ids', 1, 0),
-(4, 3, '0000-00-00 00:00:00', '', 'cesnet_sserv', '', 1, 'cz.cesnet.au1.cesnet_sserv', 1, 0),
-(5, 3, '0000-00-00 00:00:00', '', 'n6d-dorkbot', '', 1, 'cz.cesnet.au1.n6d-dorkbot', 1, 0),
-(6, 3, '0000-00-00 00:00:00', '', 'n6e-certplsinkhole', '', 1, 'cz.cesnet.au1.n6e-certplsinkhole', 1, 0),
-(7, 3, '0000-00-00 00:00:00', '', 'n6i-citadelsinkhole', '', 1, 'cz.cesnet.au1.n6i-citadelsinkhole', 1, 0),
-(8, 3, '0000-00-00 00:00:00', '', 'n6i-citadelsinkholeqd', '', 1, 'cz.cesnet.au1.n6i-citadelsinkholeqd', 1, 0),
-(9, 3, '0000-00-00 00:00:00', '', 'n6n-openntp', '', 1, 'cz.cesnet.au1.n6n-openntp', 1, 0),
-(10, 3, '0000-00-00 00:00:00', '', 'n6o-botszeroaccess', '', 1, 'cz.cesnet.au1.n6o-botszeroaccess', 1, 0),
-(11, 3, '0000-00-00 00:00:00', '', 'report_n6v-virut', '', 1, 'cz.cesnet.au1.report_n6v-virut', 1, 0),
-(12, 12, '0000-00-00 00:00:00', '', 'ids-cz', '', 1, 'cz.cesnet.au2.ids-cz', 1, 0),
-(13, 13, '0000-00-00 00:00:00', '', 'hpscan', '', 1, 'cz.vutbr.net.bee.hpscan', 1, 0),
-(14, 14, '0000-00-00 00:00:00', '', 'kippo', '', 1, 'cz.vsb.buldog.kippo', 1, 0),
-(15, 15, '0000-00-00 00:00:00', '', 'nemea', '', 1, 'org.liberouter.collector-nemea.nemea', 1, 0),
-(16, 16, '0000-00-00 00:00:00', '', 'hoststats', '', 1, 'org.liberouter.collector.hoststats', 1, 0),
-(17, 17, '0000-00-00 00:00:00', '', 'synscandetector_1_0', '', 1, 'org.liberouter.collector.synscandetector_1_0', 1, 0),
-(18, 18, '0000-00-00 00:00:00', '', 'kippohoneypot', '', 1, 'cz.cesnet.holly.kippohoneypot', 1, 0),
-(19, 19, '0000-00-00 00:00:00', '', 'dionaeahoneypot', '', 1, 'cz.cesnet.kryten.dionaeahoneypot', 1, 0),
-(20, 20, '0000-00-00 00:00:00', '', 'mentat', '', 1, 'cz.cesnet.mentat.mentat', 1, 0),
-(21, 21, '0000-00-00 00:00:00', '', 'kippo', '', 1, 'cz.slu.opf.miel.kippo', 1, 0),
-(22, 23, '0000-00-00 00:00:00', '', 'honeyscan', '', 1, 'cz.muni.ics.nfsen.honeyscan', 1, 0),
-(23, 23, '0000-00-00 00:00:00', '', 'scandetector_1_0', '', 1, 'cz.muni.ics.nfsen.scandetector_1_0', 1, 0),
-(24, 23, '0000-00-00 00:00:00', '', 'sshbruteforce-1_n', '', 1, 'cz.muni.ics.nfsen.sshbruteforce-1_n', 1, 0),
-(25, 25, '0000-00-00 00:00:00', '', 'fail2ban', '', 1, 'cz.cesnet.vinovago.fail2ban', 1, 0),
-(26, 26, '0000-00-00 00:00:00', '', 'dionaeatul', '', 1, 'cz.tul.ward.dionaeatul', 1, 0),
-(27, 26, '0000-00-00 00:00:00', '', 'kippo', '', 1, 'cz.tul.ward.kippo', 1, 0),
-(28, 28, '0000-00-00 00:00:00', 'kostenec@civ.zcu.cz', 'com.example.test-node', '', 1, 'com.example.test-node', 0, 0),
-(29, 28, '0000-00-00 00:00:00', 'kostenec@civ.zcu.cz', 'com.example.test-node2', '', 1, 'com.example.test-node2', 0, 0),
-(30, 28, '0000-00-00 00:00:00', 'kostenec@civ.zcu.cz', 'Test', '', 1, 'com.example.test-node3', 0, 0),
-(31, 29, '2014-12-11 13:51:18', 'ph@cesnet.cz', 'Test', '', 1, 'cz.cesnet.grey.test', 1, 0);
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci AUTO_INCREMENT=1 ;
 
 -- --------------------------------------------------------
 
@@ -246,7 +207,7 @@ CREATE TABLE IF NOT EXISTS `tags` (
   `tag` varchar(64) NOT NULL,
   KEY `id_tag_name` (`id`,`tag`),
   KEY `tag_name` (`tag`)
-) ENGINE=MyISAM DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
 
 --
 -- Dumping data for table `tags`
diff --git a/warden3/warden_server/warden_server.py b/warden3/warden_server/warden_server.py
index 453199a8c20f8140462090ae0b8a3b7a9dd3fb47..b7e214dbe6d96e4cb7cb66abd2987bcad6ac80f6 100755
--- a/warden3/warden_server/warden_server.py
+++ b/warden3/warden_server/warden_server.py
@@ -14,6 +14,7 @@ import M2Crypto.X509
 import json
 import MySQLdb as my
 import MySQLdb.cursors as mycursors
+from collections import namedtuple
 from uuid import uuid4
 from time import time, gmtime
 from math import trunc
@@ -57,7 +58,7 @@ class Error(Exception):
 
 
     def info_str(self):
-        return ("Detail: %s" % self.detail) or ""
+        return ("Detail: %s" % self.detail) if self.detail else ""
 
 
     def debug_str(self):
@@ -173,6 +174,13 @@ class Object(object):
 
 
 
+# Simple container class definition shortcut
+Client = namedtuple("Client",
+    ["id", "registered", "requestor", "hostname", "service", "note",
+    "identity", "read", "debug", "write", "test"])
+
+
+
 class Request(Object):
     """ Simple container for info about ongoing request.
         One instance gets created before server startup, and all other
@@ -200,9 +208,9 @@ class Request(Object):
 
 
     def reset(self, env=None, client=None, path=None, req_id=None):
-        self.env = {} if env is None else env
-        self.client = {} if client is None else client
-        self.path = "" if path is None else path
+        self.env = env
+        self.client = client
+        self.path = path or ""
         if req_id is not None:
             self.req_id = req_id
         else:
@@ -214,7 +222,6 @@ class Request(Object):
 
 
 
-
 class ObjectReq(Object):
 
     def __init__(self, req):
@@ -226,17 +233,18 @@ class ObjectReq(Object):
         return "%s(req=%s)" % (type(self).__name__, type(self.req).__name__)
 
 
+
 class NoAuthenticator(ObjectReq):
 
     def __init__(self, req):
         ObjectReq.__init__(self, req)
 
 
-    def authenticate (self, env):
+    def authenticate (self, env, args):
         return "anybody"    # or None
 
 
-    def authorize(self, env, client, path, event):
+    def authorize(self, env, client, path, method):
         return (client is not None)
 
 
@@ -268,51 +276,46 @@ class X509Authenticator(NoAuthenticator):
         return [firstcommon] + list(set(altnames+commons) - set([firstcommon]))
 
 
-    def authenticate (self, env):
-        names = self.get_cert_dns_names(env["SSL_CLIENT_CERT"])
-        return self.db.get_client_by_name(names)
-
-
-    def authorize(self, env, client, path, event):
-        # Authorize for debug
-        if (path == 'getDebug'):
-            if not client["debug"]:
-                logging.info("Auth failed: client does not have debug enabled")
-                return None
-            return client
-
-        if path in ['getInfo', 'getEvents']:
-            return client
-
+    def authenticate (self, env, args):
         try:
-            identity = event['Node'][0]['Name'].lower()
-        except (KeyError, TypeError):
-            # Event does not bear valid Node attribute
-            logging.info("Auth failed: event does not bear valid Node attribute")
+            identity = args["client"][0]
+        except KeyError:
+            logging.info("authenticate: bad or missing client argument")
             return None
 
         try:
-            service = client["services"][identity]
-        except KeyError:
-            # We are unable to pair service in message to service in db
-            logging.info("authorize: failed, '%s' from event not found in services for client %i" % (identity, client["id"]))
+            cert_names = self.get_cert_dns_names(env["SSL_CLIENT_CERT"])
+        except:
+            logging.info("authenticate: cannot get or parse certificate from env")
             return None
+            
+        client = self.db.get_client_by_name(identity, cert_names)
 
-        client["service"] = service
+        if not client:
+            logging.info("authenticate: client not found")
         
-        # Authorize for sending events
-        if (path == "sendEvents"):
-            if not (service["write"] or service["test"]):
-                logging.info("authorize: failed, service %i (%s) is not allowed to write or test" % (service["service_id"], identity))
+        logging.info("authenticate: ok, client=%s" % str(client))
+        return client
+
+
+    def authorize(self, env, client, path, method):
+        if method.debug:
+            if not client.debug:
+                logging.info("authorize: failed, client does not have debug enabled")
                 return None
+            return client
 
-            test = 'Test' in event.get('Category', [])
-            # if not test:
-            #     logging.info("authorize: failed, service %i (%s) does not send Test category in event" % (service["service_id"], identity))
-            if test and not service['test']:
-                logging.info("authorize: failed, service %i (%s) is not allowed to send Test category in event" % (service["service_id"], identity))
+        if method.read:
+            if not client.read:
+                logging.info("authorize: failed, client does not have read enabled")
                 return None
-        
+            return client
+
+        if method.write:
+            if not (client.write or client.test):
+                logging.info("authorize: failed, client is not allowed to write or test")
+                return None
+
         return client
         
 
@@ -393,27 +396,18 @@ class MySQL(ObjectReq):
             type(self).__name__, type(self.req).__name__, self.host, self.user, self.dbname, self.port, self.catmap_filename, self.tagmap_filename)
 
 
-    def get_client_by_name(self, name):
-        format_strings = ','.join(['%s'] * len(name))
-        self.crs.execute("SELECT cl.`id`, cl.`hostname`, s.`service`, s.`service_id`, s.`identity`, cl.`read`, s.`write`, s.`test`, cl.`debug` FROM `clients` cl LEFT JOIN `services` s ON cl.`id` = s.`client_id` WHERE cl.`valid` = 1 AND s.`valid` = 1 AND `hostname` IN (%s)" % format_strings, tuple(name))
+    def get_client_by_name(self, identity, cert_names):
+        format_strings = ','.join(['%s'] * len(cert_names))
+        query = "SELECT id, registered, requestor, hostname, service, note, identity, `read`, debug, `write`, test FROM clients WHERE valid = 1 AND identity = %%s AND hostname IN (%s)" % format_strings
+        self.crs.execute(query, [identity] + cert_names)
         rows = self.crs.fetchall()
-        if not rows:
-            return None
-
-        client = {}
-        for n in ["id", "hostname", "read", "debug"]:
-            client[n] = rows[0][n]
 
-        services = {}
-        for row in rows:
-            service = {}
-            for n in ["service", "service_id", "identity", "write", "test"]:
-                service[n] = row[n]
-            services[row["identity"]] = service
+        if len(rows)>1:
+            logging.warn("get_client_by_name: query returned more than one result: %s" % str(rows))
+            return None
 
-        client["services"] = services
+        client = Client(**rows[0]) if rows else None
 
-        logging.debug("get_client_by_name: %s", str(client))
         return client
 
 
@@ -475,7 +469,7 @@ class MySQL(ObjectReq):
         sqlwhere = []
         sqlparams = []
 
-        sqlwhere.append("SELECT e.id, e.data FROM services s RIGHT JOIN events e ON s.service_id = e.service_id WHERE e.id > %s")
+        sqlwhere.append("SELECT e.id, e.data FROM clients c RIGHT JOIN events e ON c.id = e.client_id WHERE e.id > %s")
         sqlparams.append(id or 0)
 
         if cat or nocat:
@@ -497,7 +491,7 @@ class MySQL(ObjectReq):
             not_op = "" if group else "NOT"
             
             for identity in (group or nogroup):
-                sqlwhere.append(" AND s.identity %s LIKE %%s" % not_op)
+                sqlwhere.append(" AND c.identity %s LIKE %%s" % not_op)
                 sqlparams.append(identity + "%")
 
         sqlwhere.append(" AND e.valid = 1 LIMIT %s")
@@ -525,7 +519,7 @@ class MySQL(ObjectReq):
 
     def store_event(self, client, event):
         try:
-            self.crs.execute("INSERT INTO events (received,service_id,data) VALUES (NOW(), %s, %s)", (client["service"]["service_id"], json.dumps(event)))
+            self.crs.execute("INSERT INTO events (received,client_id,data) VALUES (NOW(), %s, %s)", (client.id, json.dumps(event)))
             lastid = self.crs.lastrowid
             logging.debug("store_event: Last ID in events - %i" % lastid)
 
@@ -552,8 +546,8 @@ class MySQL(ObjectReq):
 
 
     def insertLastReceivedId(self, client, id):
-        logging.debug("insertLastReceivedId: id %i for client %i(%s)" % (id, client["id"], client["hostname"]))
-        self.crs.execute("INSERT INTO last_events(client_id, event_id, timestamp) VALUES(%s, %s, NOW())", (client["id"], id))
+        logging.debug("insertLastReceivedId: id %i for client %i(%s)" % (id, client.id, client.hostname))
+        self.crs.execute("INSERT INTO last_events(client_id, event_id, timestamp) VALUES(%s, %s, NOW())", (client.id, id))
 
     def getLastEventId(self):
         self.crs.execute("SELECT MAX(id) as id FROM events")
@@ -562,19 +556,26 @@ class MySQL(ObjectReq):
         return row['id'] if row['id'] is not None else 0
 
     def getLastReceivedId(self, client):
-        self.crs.execute("SELECT MAX(event_id) as id FROM last_events WHERE client_id = %s", client["id"])
+        self.crs.execute("SELECT MAX(event_id) as id FROM last_events WHERE client_id = %s", client.id)
         row = self.crs.fetchone()
 
         id = row['id'] if row is not None else 0
-        logging.debug("getLastReceivedId: id %i for client %i(%s)" % (id, client["id"], client["hostname"]))
+        logging.debug("getLastReceivedId: id %i for client %i(%s)" % (id, client.id, client.hostname))
 
         return id
 
 
 
-def expose(meth):
-    meth.exposed = True
-    return meth
+def expose(read=1, write=0, debug=0):
+
+    def expose_deco(meth):
+        meth.exposed = True
+        meth.read = read
+        meth.write = write
+        meth.debug = debug
+        return meth
+
+    return expose_deco
 
 
 class Server(ObjectReq):
@@ -600,7 +601,7 @@ class Server(ObjectReq):
 
         # silently remove surplus arguments - potential forward
         # compatibility (unknown args will get ignored)
-        badargs = set(args)-set(func.func_code.co_varnames[0:func.func_code.co_argcount])
+        badargs = set(args) - set(func.func_code.co_varnames[0:func.func_code.co_argcount])
         for a in badargs:
             del args[a]
         if badargs:
@@ -629,21 +630,26 @@ class Server(ObjectReq):
             except Exception:
                 raise self.req.error("You've fallen of the cliff.", 404)
 
-            self.req.client = client = self.auth.authenticate(environ)
+            self.req.args = args = parse_qs(environ.get('QUERY_STRING', ""))
+            logging.debug("arguments: %s" % str(args))
+
+            self.req.client = client = self.auth.authenticate(environ, args)
             if not client:
                 raise self.req.error("I'm watching. Authenticate.", 403)
 
             try:
                 events = json.loads(injson) if injson else None
             except Exception as e:
-                raise self.req.error("Deserialization error", 400,
+                raise self.req.error("Deserialization error.", 400,
                     sys.exc_info(), detail={"args": injson, "parser": str(e)})
-
-            args = parse_qs(environ.get('QUERY_STRING', ""))
-            logging.debug("calling handler with %s" % str(args))
             if events:
                 args["events"] = events
 
+            auth = self.auth.authorize(self.req.env, self.req.client, self.req.path, method)
+            if not auth:
+                raise self.req.error("I'm watching. Not authorized.", 403, detail={"client": client.identity})
+
+            args.pop("client", None)
             args = self.sanitize_args(path, method, args)
             result = method(**args)   # call requested method
 
@@ -709,15 +715,11 @@ class WardenHandler(ObjectReq):
             self.get_events_limit, self.send_events_limit, self.description)
 
 
-    @expose
+    @expose(read=1, debug=1)
     def getDebug(self):
-        auth = self.auth.authorize(self.req.env, self.req.client, self.req.path, None)
-        if not auth:
-            raise self.req.error("I'm watching. Authorize.", 403, detail={"client": self.req.client["hostname"]})
-
         return {
             "environment": self.req.env,
-            "client": self.req.client,
+            "client": self.req.client.__dict__,
             "database": self.db.get_debug(),
             "system": {
                 "uname": os.uname()
@@ -736,12 +738,8 @@ class WardenHandler(ObjectReq):
         }
 
 
-    @expose
+    @expose(read=1)
     def getInfo(self):
-        auth = self.auth.authorize(self.req.env, self.req.client, self.req.path, None)
-        if not auth:
-            raise self.req.error("I'm watching. Authorize.", 403, detail={"client": self.req.client["hostname"]})
-
         info = {
             "version": VERSION,
             "send_events_limit": self.send_events_limit,
@@ -752,16 +750,12 @@ class WardenHandler(ObjectReq):
         return info
 
 
-    @expose
+    @expose(read=1)
     def getEvents(self, id=None, count=None,
             cat=None, nocat=None,
             tag=None, notag=None,
             group=None, nogroup=None):
 
-        auth = self.auth.authorize(self.req.env, self.req.client, self.req.path, None)
-        if not auth:
-            raise self.req.error("I'm watching. Authorize.", 403, detail={"client": self.req.client["hostname"]})
-
         try:
             id = int(id[0])
         except (ValueError, TypeError, IndexError):
@@ -799,7 +793,18 @@ class WardenHandler(ObjectReq):
         return res
 
 
-    @expose
+    def checkNode(self, event, identity):
+        try:
+            ev_id = event['Node'][0]['Name'].lower()
+        except (KeyError, TypeError):
+            # Event does not bear valid Node attribute
+            return ["Event does not bear valid Node attribute"]
+        if ev_id != identity:
+            return ["Node does not correspond with saving client"]
+        return []
+
+
+    @expose(write=1)
     def sendEvents(self, events=[]):
         if not isinstance(events, list):
             raise self.req.error("List of events expected.", 400)
@@ -811,19 +816,20 @@ class WardenHandler(ObjectReq):
         saved = 0
         errs = {}
         for i, event in enumerate(events):
-            ev_errs = []
-            
-            auth_cl = self.auth.authorize(self.req.env, self.req.client, self.req.path, event)
-            if not auth_cl:
-                errs[i] = ["Client %i(%s) does not correspond with event Node info or is not allowed to write" % (self.req.client["service"]["service_id"], self.req.client["service"]["identity"])]
-                continue
-
             v_errs = self.validator.check(event)
             if v_errs:
                 errs[i] = v_errs
                 continue
 
-            db_errs = self.db.store_event(auth_cl, event)
+            node_errs = self.checkNode(event, self.req.client.identity)
+            if node_errs:
+                errs[i] = node_errs
+                continue
+
+            if self.req.client.test and not 'Test' in event.get('Category', []):
+                errs[i] = ["You're allowed to send only messages, containing \"Test\" among categories."]
+
+            db_errs = self.db.store_event(self.req.client, event)
             if db_errs:
                 errs[i] = db_errs
                 continue