a3470f
From 21154511978486010405a1d3a826d46dd8b9d324 Mon Sep 17 00:00:00 2001
a3470f
From: Aravinda VK <avishwan@redhat.com>
a3470f
Date: Mon, 18 Sep 2017 14:34:54 +0530
a3470f
Subject: [PATCH 114/128] eventsapi: Add JWT signing support
a3470f
a3470f
New argument added to accept secret to generate JWT token. This patch
a3470f
does not affect the backward compatibility.
a3470f
a3470f
Usage:
a3470f
a3470f
    gluster-eventsapi webhook-add <url> [-t <TOKEN>] \
a3470f
        [-s SECRET]
a3470f
a3470f
With `--token` argument, Token header will be added as is.
a3470f
a3470f
    Authorization: Bearer <TOKEN>
a3470f
a3470f
In case of shared secret, Gluster will generate JWT token using the
a3470f
secret and then add it to Authorization header.
a3470f
a3470f
    Authorization: Bearer <GENERATED_TOKEN>
a3470f
a3470f
Secret/Token can be updated using `webhook-mod` command.
a3470f
a3470f
Generated token will include the following payload,
a3470f
a3470f
    {
a3470f
       "iss": "gluster",
a3470f
       "exp": EXPIRY_TIME,
a3470f
       "sub": EVENT_TYPE,
a3470f
       "iat": EVENT_TIME
a3470f
     }
a3470f
a3470f
Where: iss - Issuer, exp - Expiry Time, sub - Event Type
a3470f
       used as Subject, iat - Event Time used as Issue Time
a3470f
a3470f
>upstream mainline patch : https://review.gluster.org/#/c/18405
a3470f
a3470f
BUG: 1466129
a3470f
Change-Id: Ib6b6fab23fb212d7f5e9bbc9e1416a9e9813ab1b
a3470f
Signed-off-by: Aravinda VK <avishwan@redhat.com>
a3470f
Reviewed-on: https://code.engineering.redhat.com/gerrit/126551
a3470f
Tested-by: RHGS Build Bot <nigelb@redhat.com>
a3470f
---
a3470f
 events/src/peer_eventsapi.py | 40 ++++++++++++++++++++++++++++++++++-----
a3470f
 events/src/utils.py          | 45 +++++++++++++++++++++++++++++++++++---------
a3470f
 glusterfs.spec.in            |  4 ++--
a3470f
 3 files changed, 73 insertions(+), 16 deletions(-)
a3470f
a3470f
diff --git a/events/src/peer_eventsapi.py b/events/src/peer_eventsapi.py
a3470f
index 59808ad..3a6a0eb 100644
a3470f
--- a/events/src/peer_eventsapi.py
a3470f
+++ b/events/src/peer_eventsapi.py
a3470f
@@ -18,6 +18,7 @@ import fcntl
a3470f
 from errno import EACCES, EAGAIN
a3470f
 import signal
a3470f
 import sys
a3470f
+import time
a3470f
 
a3470f
 import requests
a3470f
 from prettytable import PrettyTable
a3470f
@@ -26,7 +27,7 @@ from gluster.cliutils import (Cmd, node_output_ok, node_output_notok,
a3470f
                               sync_file_to_peers, GlusterCmdException,
a3470f
                               output_error, execute_in_peers, runcli,
a3470f
                               set_common_args_func)
a3470f
-from events.utils import LockedOpen
a3470f
+from events.utils import LockedOpen, get_jwt_token
a3470f
 
a3470f
 from events.eventsapiconf import (WEBHOOKS_FILE_TO_SYNC,
a3470f
                                   WEBHOOKS_FILE,
a3470f
@@ -307,6 +308,8 @@ class WebhookAddCmd(Cmd):
a3470f
         parser.add_argument("url", help="URL of Webhook")
a3470f
         parser.add_argument("--bearer_token", "-t", help="Bearer Token",
a3470f
                             default="")
a3470f
+        parser.add_argument("--secret", "-s",
a3470f
+                            help="Secret to add JWT Bearer Token", default="")
a3470f
 
a3470f
     def run(self, args):
a3470f
         create_webhooks_file_if_not_exists(args)
a3470f
@@ -318,7 +321,8 @@ class WebhookAddCmd(Cmd):
a3470f
                                     errcode=ERROR_WEBHOOK_ALREADY_EXISTS,
a3470f
                                     json_output=args.json)
a3470f
 
a3470f
-            data[args.url] = args.bearer_token
a3470f
+            data[args.url] = {"token": args.bearer_token,
a3470f
+                              "secret": args.secret}
a3470f
             file_content_overwrite(WEBHOOKS_FILE, data)
a3470f
 
a3470f
         sync_to_peers(args)
a3470f
@@ -331,6 +335,8 @@ class WebhookModCmd(Cmd):
a3470f
         parser.add_argument("url", help="URL of Webhook")
a3470f
         parser.add_argument("--bearer_token", "-t", help="Bearer Token",
a3470f
                             default="")
a3470f
+        parser.add_argument("--secret", "-s",
a3470f
+                            help="Secret to add JWT Bearer Token", default="")
a3470f
 
a3470f
     def run(self, args):
a3470f
         create_webhooks_file_if_not_exists(args)
a3470f
@@ -342,7 +348,16 @@ class WebhookModCmd(Cmd):
a3470f
                                     errcode=ERROR_WEBHOOK_NOT_EXISTS,
a3470f
                                     json_output=args.json)
a3470f
 
a3470f
-            data[args.url] = args.bearer_token
a3470f
+            if isinstance(data[args.url], str) or \
a3470f
+               isinstance(data[args.url], unicode):
a3470f
+                data[args.url]["token"] = data[args.url]
a3470f
+
a3470f
+            if args.bearer_token != "":
a3470f
+                data[args.url]["token"] = args.bearer_token
a3470f
+
a3470f
+            if args.secret != "":
a3470f
+                data[args.url]["secret"] = args.secret
a3470f
+
a3470f
             file_content_overwrite(WEBHOOKS_FILE, data)
a3470f
 
a3470f
         sync_to_peers(args)
a3470f
@@ -376,11 +391,19 @@ class NodeWebhookTestCmd(Cmd):
a3470f
     def args(self, parser):
a3470f
         parser.add_argument("url")
a3470f
         parser.add_argument("bearer_token")
a3470f
+        parser.add_argument("secret")
a3470f
 
a3470f
     def run(self, args):
a3470f
         http_headers = {}
a3470f
+        hashval = ""
a3470f
         if args.bearer_token != ".":
a3470f
-            http_headers["Authorization"] = "Bearer " + args.bearer_token
a3470f
+            hashval = args.bearer_token
a3470f
+
a3470f
+        if args.secret != ".":
a3470f
+            hashval = get_jwt_token(args.secret, "TEST", int(time.time()))
a3470f
+
a3470f
+        if hashval:
a3470f
+            http_headers["Authorization"] = "Bearer " + hashval
a3470f
 
a3470f
         try:
a3470f
             resp = requests.post(args.url, headers=http_headers)
a3470f
@@ -401,16 +424,23 @@ class WebhookTestCmd(Cmd):
a3470f
     def args(self, parser):
a3470f
         parser.add_argument("url", help="URL of Webhook")
a3470f
         parser.add_argument("--bearer_token", "-t", help="Bearer Token")
a3470f
+        parser.add_argument("--secret", "-s",
a3470f
+                            help="Secret to generate Bearer Token")
a3470f
 
a3470f
     def run(self, args):
a3470f
         url = args.url
a3470f
         bearer_token = args.bearer_token
a3470f
+        secret = args.secret
a3470f
+
a3470f
         if not args.url:
a3470f
             url = "."
a3470f
         if not args.bearer_token:
a3470f
             bearer_token = "."
a3470f
+        if not args.secret:
a3470f
+            secret = "."
a3470f
 
a3470f
-        out = execute_in_peers("node-webhook-test", [url, bearer_token])
a3470f
+        out = execute_in_peers("node-webhook-test", [url, bearer_token,
a3470f
+                                                     secret])
a3470f
 
a3470f
         if not args.json:
a3470f
             table = PrettyTable(["NODE", "NODE STATUS", "WEBHOOK STATUS"])
a3470f
diff --git a/events/src/utils.py b/events/src/utils.py
a3470f
index 2a77b13..5130720 100644
a3470f
--- a/events/src/utils.py
a3470f
+++ b/events/src/utils.py
a3470f
@@ -13,10 +13,11 @@ import json
a3470f
 import os
a3470f
 import logging
a3470f
 import fcntl
a3470f
-from errno import ESRCH, EBADF
a3470f
+from errno import EBADF
a3470f
 from threading import Thread
a3470f
 import multiprocessing
a3470f
 from Queue import Queue
a3470f
+from datetime import datetime, timedelta
a3470f
 
a3470f
 from eventsapiconf import (LOG_FILE,
a3470f
                            WEBHOOKS_FILE,
a3470f
@@ -183,15 +184,33 @@ def autoload_webhooks():
a3470f
             load_webhooks()
a3470f
 
a3470f
 
a3470f
-def publish_to_webhook(url, token, message_queue):
a3470f
+def get_jwt_token(secret, event_type, event_ts, jwt_expiry_time_seconds=60):
a3470f
+    import jwt
a3470f
+    payload = {
a3470f
+        "exp": datetime.utcnow() + timedelta(seconds=jwt_expiry_time_seconds),
a3470f
+        "iss": "gluster",
a3470f
+        "sub": event_type,
a3470f
+        "iat": event_ts
a3470f
+    }
a3470f
+    return jwt.encode(payload, secret, algorithm='HS256')
a3470f
+
a3470f
+
a3470f
+def publish_to_webhook(url, token, secret, message_queue):
a3470f
     # Import requests here since not used in any other place
a3470f
     import requests
a3470f
 
a3470f
     http_headers = {"Content-Type": "application/json"}
a3470f
     while True:
a3470f
-        message_json = message_queue.get()
a3470f
+        hashval = ""
a3470f
+        event_type, event_ts, message_json = message_queue.get()
a3470f
         if token != "" and token is not None:
a3470f
-            http_headers["Authorization"] = "Bearer " + token
a3470f
+            hashval = token
a3470f
+
a3470f
+        if secret != "" and secret is not None:
a3470f
+            hashval = get_jwt_token(secret, event_type, event_ts)
a3470f
+
a3470f
+        if hashval:
a3470f
+            http_headers["Authorization"] = "Bearer " + hashval
a3470f
 
a3470f
         try:
a3470f
             resp = requests.post(url, headers=http_headers, data=message_json)
a3470f
@@ -218,7 +237,7 @@ def publish_to_webhook(url, token, message_queue):
a3470f
 def plugin_webhook(message):
a3470f
     message_json = json.dumps(message, sort_keys=True)
a3470f
     logger.debug("EVENT: {0}".format(message_json))
a3470f
-    webhooks_pool.send(message_json)
a3470f
+    webhooks_pool.send(message["event"], message["ts"], message_json)
a3470f
 
a3470f
 
a3470f
 class LockedOpen(object):
a3470f
@@ -298,9 +317,17 @@ class PidFile(object):
a3470f
 
a3470f
 def webhook_monitor(proc_queue, webhooks):
a3470f
     queues = {}
a3470f
-    for url, token in webhooks.items():
a3470f
+    for url, data in webhooks.items():
a3470f
+        if isinstance(data, str) or isinstance(data, unicode):
a3470f
+            token = data
a3470f
+            secret = None
a3470f
+        else:
a3470f
+            token = data["token"]
a3470f
+            secret = data["secret"]
a3470f
+
a3470f
         queues[url] = Queue()
a3470f
-        t = Thread(target=publish_to_webhook, args=(url, token, queues[url]))
a3470f
+        t = Thread(target=publish_to_webhook, args=(url, token, secret,
a3470f
+                                                    queues[url]))
a3470f
         t.start()
a3470f
 
a3470f
     # Get the message sent to Process queue and distribute to all thread queues
a3470f
@@ -329,8 +356,8 @@ class WebhookThreadPool(object):
a3470f
         self.proc.terminate()
a3470f
         self.start()
a3470f
 
a3470f
-    def send(self, message):
a3470f
-        self.queue.put(message)
a3470f
+    def send(self, event_type, event_ts, message):
a3470f
+        self.queue.put((event_type, event_ts, message))
a3470f
 
a3470f
 
a3470f
 def init_webhook_pool():
a3470f
diff --git a/glusterfs.spec.in b/glusterfs.spec.in
a3470f
index 56a62a9..29329fa 100644
a3470f
--- a/glusterfs.spec.in
a3470f
+++ b/glusterfs.spec.in
a3470f
@@ -671,9 +671,9 @@ Requires:         %{name}-server%{?_isa} = %{version}-%{release}
a3470f
 Requires:         python2 python-prettytable
a3470f
 Requires:         python2-gluster = %{version}-%{release}
a3470f
 %if ( 0%{?rhel} )
a3470f
-Requires:         python-requests
a3470f
+Requires:         python-requests python-jwt
a3470f
 %else
a3470f
-Requires:         python2-requests
a3470f
+Requires:         python2-requests python2-jwt
a3470f
 %endif
a3470f
 %if ( 0%{?rhel} && 0%{?rhel} < 7 )
a3470f
 Requires:         python-argparse
a3470f
-- 
a3470f
1.8.3.1
a3470f