|
|
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 |
|