diff --git a/docs/source/user/use-cases.rst b/docs/source/user/use-cases.rst
index bbfdaf4a..5c5555c8 100644
--- a/docs/source/user/use-cases.rst
+++ b/docs/source/user/use-cases.rst
@@ -496,7 +496,41 @@ If not testing, obtain a valid STIR/SHAKEN certificate and place them in the /et
11. Click Save
-The STIR/SHAKEN page should look like this:
+Configure Dynamic SIP Credentials (SUBSCRIPTION REQUIRED)
+---------------------------------------------------------
-.. image:: images/stir_shaken_settings.png
- :align: center
+dSIPRouter enables an organization to provide users and systems with SIP credentials that will expire after a time-to-live (ttl). The goal is to minimize the attack surface and user error that would cause SIP credentials to become compromised. The steps to configure is below:
+
+1. Login to dSIPRouter
+2. Purchase a core subscription license from the `dSIPRouter Marketplace `_
+3. Click System Settings -> License Manager
+4. Add the license to the system
+5. Get or set the API Token. You can set the API token using the command below:
+
+.. code-block:: bash
+
+ DSIP_HOSTNAME=
+ DSIP_TOKEN=
+ dsiprouter setcredentials -ac $DSIP_TOKEN
+
+6. Invoke the Lease API, which can be found in the API section of the `dSIPRouter Postman `_. There are two types of SIP Credentials supported, user/pass credentials and IP based.
+
+User/Pass Credential
+
+.. code-block:: bash
+
+ curl -k -H "Authorization: Bearer $DSIP_TOKEN" -H "Content-Type: application/json" -X GET "https://$DSIP_HOSTNAME:5000/api/v1/lease/endpoint?ttl=15&email=mack@dsiprouter.org"
+
+IP Based Credential
+
+.. code-block:: bash
+
+ curl -k -H "Authorization: Bearer $DSIP_TOKEN" -H "Content-Type: application/json" -X GET "https://$DSIP_HOSTNAME:5000/api/v1/lease/endpoint?email=mack@goflyball.com&ttl=15m&type=ip&auth_ip=172.145.24.2"
+
+7. You can revoke the lease id that was returned when the lease was created:
+
+.. code-block:: bash
+
+ curl -k -H "Authorization: Bearer $DSIP_TOKEN" -H "Content-Type: application/json" -X DELETE "https://$DSIP_HOSTNAME:5000/api/v1/lease/endpoint/</revoke"
+
+8. (Optional) You can set the SIP Domain used for generating the User/Pass credentials by changing the DEFAULT_AUTH_DOMAIN parameter in /etc/dsiprouter/gui/settings.py
diff --git a/gui/database/__init__.py b/gui/database/__init__.py
index f524de7a..7deba424 100644
--- a/gui/database/__init__.py
+++ b/gui/database/__init__.py
@@ -291,12 +291,12 @@ class dSIPLeases(object):
maintains a list of active leases based on seconds
"""
- def __init__(self, gwid, sid, ttl):
+ def __init__(self, gwid, sid, ttl, addrid=None):
self.gwid = gwid
self.sid = sid
t = datetime.now() + timedelta(seconds=ttl)
self.expiration = t.strftime('%Y-%m-%d %H:%M:%S')
-
+ self.addrid = addrid
pass
diff --git a/gui/modules/api/api.sql b/gui/modules/api/api.sql
index cee08ea9..f7d72604 100644
--- a/gui/modules/api/api.sql
+++ b/gui/modules/api/api.sql
@@ -3,6 +3,7 @@ CREATE TABLE `dsip_endpoint_lease` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`gwid` int(10) unsigned NOT NULL,
`sid` int(10) unsigned NOT NULL,
+ `addrid` int(10) unsigned NULL,
`expiration` datetime NOT NULL,
PRIMARY KEY (`id`)
);
diff --git a/gui/modules/api/api_functions.py b/gui/modules/api/api_functions.py
index 98347140..54482c40 100644
--- a/gui/modules/api/api_functions.py
+++ b/gui/modules/api/api_functions.py
@@ -11,7 +11,7 @@
from werkzeug import exceptions as http_exceptions
from shared import debugException, StatusCodes
from util.ipc import STATE_SHMEM_NAME, getSharedMemoryDict
-from util.security import APIToken
+from util.security import APIToken, validSlackSignature
from modules.api.licensemanager.functions import getLicenseStatus
import settings
@@ -110,6 +110,14 @@ def wrapper(*args, **kwargs):
msg='Unauthorized - Core Subscription Required. Purchase from https://dopensource.com/product/dsiprouter-core/',
status_code=StatusCodes.HTTP_UNAUTHORIZED
)
+ # Check if request is from Slack and the endpoint is the leasing endpoint
+ if request.headers.get('X-Slack-Signature'):
+ #flask_rule[:17] == "/api/v1/lease/endpoint":
+ if validSlackSignature(settings.SLACK_SIGNING_SECRET,request):
+ # checks succeeded allow the request
+ return func(*args, **kwargs)
+ else:
+ return jsonify(msg='Unauthorized: Slack signing key is missing or invalid')
# Check if token is valid
if not apiToken.isValid():
return createApiResponse(
diff --git a/gui/modules/api/api_routes.py b/gui/modules/api/api_routes.py
index 8200e934..c40cde4e 100644
--- a/gui/modules/api/api_routes.py
+++ b/gui/modules/api/api_routes.py
@@ -21,7 +21,7 @@
from util.ipc import STATE_SHMEM_NAME, getSharedMemoryDict
from modules.api.api_functions import createApiResponse, showApiError, api_security
from modules.api.kamailio.functions import reloadKamailio
-from util.networking import getExternalIP, hostToIP, safeUriToHost, safeStripPort
+from util.networking import getExternalIP, hostToIP, safeUriToHost, safeStripPort, isValidIP
from util.notifications import sendEmail
from util.security import AES_CTR, urandomChars, KeyCertPair
from util.file_handling import change_owner
@@ -130,7 +130,7 @@ def handleReloadDsiprouter():
# it should be renamed and changed to use POST method
# TODO: the last lease id/username generated must be tracked (just query DB)
# and used to determine next lease id, otherwise conflicts may occur
-@api.route("/api/v1/lease/endpoint", methods=['GET'])
+@api.route("/api/v1/lease/endpoint", methods=['GET','POST'])
@api_security
def getEndpointLease():
db = DummySession()
@@ -145,13 +145,62 @@ def getEndpointLease():
db = startSession()
- email = request.args.get('email')
- if not email:
- raise Exception("email parameter is missing")
- ttl = request.args.get('ttl', None)
- if ttl is None:
- raise http_exceptions.BadRequest("time to live (ttl) parameter is missing")
+ # Grab the form parameters from Slack request
+
+ # Set up some defaults
+ ttl = "5m"
+ type = "userpwd"
+ auth_ip = None
+
+ if request.headers.get('X-Slack-Signature'):
+ text = request.form.get('text', None)
+ if text is not None and len(text) > 0:
+ print("**** {}".format(text))
+ # Split text up
+ x=text.split(" ")
+ for req in x:
+ if "m" in req:
+ ttl = req
+ if "userpwd" == req or "ip" == req:
+ type = req
+ if "/" in req:
+ ip,subnet=req.split("/")
+ if isValidIP(ip):
+ auth_ip = req
+
+ if type == "ip" and auth_ip is None:
+ raise http_exceptions.BadRequest("auth_ip must be provided if the type is ip")
+ elif type == "userpwd" and auth_ip is not None:
+ raise http_exceptions.BadRequest("auth_ip is not valid when type is userpwd")
+
+ #Grab email address
+ email = request.form.get('email', None)
+ #Set content type equal to application/json
+ request.ContentType = 'application/json'
+
+ # Grab request parameters from everywhere else
+ else:
+ ttl = request.args.get('ttl')
+ if ttl is None:
+ raise http_exceptions.BadRequest("time to live (ttl) parameter is missing")
+
+ email = request.args.get('email')
+ if not email:
+ raise Exception("email parameter is missing")
+
+ type = request.args.get('type', None)
+ if type is None:
+ type = "userpwd"
+ elif type != "userpwd" and type != "ip":
+ raise http_exceptions.BadRequest("type can only have a value of ip or userpwd")
+
+ auth_ip = request.args.get('auth_ip', None)
+ if type == "ip" and auth_ip is None:
+ raise http_exceptions.BadRequest("auth_ip must be provided if the type is ip")
+ elif type == "userpwd" and auth_ip is not None:
+ raise http_exceptions.BadRequest("auth_ip is not valid when type is userpwd")
+
# Convert TTL to Seconds
r = re.compile('\d*m|M')
@@ -161,33 +210,57 @@ def getEndpointLease():
# Generate some values
rand_num = random.randint(1, 200)
name = "lease" + str(rand_num)
- auth_username = name
- auth_password = urandomChars(DEF_PASSWORD_LEN)
auth_domain = settings.DEFAULT_AUTH_DOMAIN
+
+ if type == "userpwd":
+ auth_username = name
+ auth_password = urandomChars(DEF_PASSWORD_LEN)
+
+ # Set some defaults
+ host_addr = ''
+ strip = 0
+ prefix = ''
+
+ # Add the Gateways table
+ Gateway = Gateways(name, host_addr, strip, prefix, settings.FLT_PBX)
+ db.add(Gateway)
+ db.flush()
- # Set some defaults
- host_addr = ''
- strip = 0
- prefix = ''
+ # Add the Subscribers table
+ Subscriber = Subscribers(auth_username, auth_password, auth_domain, Gateway.gwid, email)
+ db.add(Subscriber)
+ db.flush()
+
+ # Add to the Leases table
+ Lease = dSIPLeases(Gateway.gwid, Subscriber.id, int(ttl))
+
+ if type == "ip":
+ # Check for Subnet
+ if "/" in auth_ip:
+ ip,subnet = auth_ip.split("/",1)
+ else:
+ ip = auth_ip
+ # Define a default subnet address of 32
+ subnet = 32
+ Addr = Address(name, ip, subnet, settings.FLT_PBX, gwgroup=2200)
+ db.add(Addr)
+ db.flush()
- # Add the Gateways table
- Gateway = Gateways(name, host_addr, strip, prefix, settings.FLT_PBX)
- db.add(Gateway)
- db.flush()
+ # Add to the Leases table
+ Lease = dSIPLeases(gwid=0, sid=0, ttl=int(ttl), addrid=Addr.id)
- # Add the Subscribers table
- Subscriber = Subscribers(auth_username, auth_password, auth_domain, Gateway.gwid, email)
- db.add(Subscriber)
- db.flush()
+
- # Add to the Leases table
- Lease = dSIPLeases(Gateway.gwid, Subscriber.id, int(ttl))
db.add(Lease)
db.flush()
lease_data['leaseid'] = Lease.id
- lease_data['username'] = auth_username
- lease_data['password'] = auth_password
+ if type == "userpwd":
+ lease_data['username'] = auth_username
+ lease_data['password'] = auth_password
+ elif type == "ip":
+ lease_data['allowed_ip'] = auth_ip
+
lease_data['domain'] = auth_domain
lease_data['ttl'] = ttl
@@ -198,11 +271,11 @@ def getEndpointLease():
#if not addTaggedCronjob("lease_management", "* * * * *", cron_cmd):
# raise Exception('Crontab entry could not be created')
- getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = True
+ getSharedMemoryDict(STATE_SHMEM_NAME)['kam_reload_required'] = False
return createApiResponse(
msg='Lease created',
data=[lease_data],
- kamreload=True,
+ kamreload=False,
)
except Exception as ex:
diff --git a/gui/settings.py b/gui/settings.py
index dc81ee3a..6a09842e 100644
--- a/gui/settings.py
+++ b/gui/settings.py
@@ -232,4 +232,8 @@
# example for ldap module:
# AUTH_MODULES = {"ldap": {"LDAP_HOST":"ldap://ldap.dopensource.com", "USER_SEARCH_BASE":"ou=People,dc=dopensource,dc=com", "GROUP_SEARCH_BASE":"dc=dopensource,dc=com", "GROUP_MEMBER_ATTRIBUTE":"memberUid", "REQUIRED_GROUP":"support", "USER_ATTRIBUTE":"uid"}}
AUTH_MODULES = {}
+
+# slack settings
+SLACK_SIGNING_SECRET = ''
############### End Local-Only Settings ##################
+
diff --git a/gui/util/security.py b/gui/util/security.py
index 6bd2664e..94a4254d 100644
--- a/gui/util/security.py
+++ b/gui/util/security.py
@@ -4,7 +4,8 @@
if sys.path[0] != '/etc/dsiprouter/gui':
sys.path.insert(0, '/etc/dsiprouter/gui')
-import os, hashlib, binascii, string, ssl, OpenSSL, secrets, re
+import os, hashlib, binascii, string, ssl, OpenSSL, secrets, re, hmac
+import urllib.parse
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from shared import updateConfig, StatusCodes
@@ -482,3 +483,32 @@ def dumpPkey(self, encoding=OpenSSL.crypto.FILETYPE_PEM):
def dumpCerts(self, encoding=OpenSSL.crypto.FILETYPE_PEM):
return b'\n'.join([OpenSSL.crypto.dump_certificate(encoding, cert) for cert in self.certs])
+
+def validSlackSignature(slack_signing_secret,request):
+
+ timestamp = request.headers['X-Slack-Request-Timestamp']
+ slack_payload = request.form
+ dict_slack = slack_payload.to_dict()
+
+ payload= "&".join(['='.join([key, urllib.parse.quote(val, safe='')]) for key, val in dict_slack.items()])
+
+ ### compose the message:
+ sig_basestring = 'v0:' + timestamp + ':' + payload
+
+ sig_basestring = sig_basestring.encode('utf-8')
+
+ ## secret
+ signing_secret = slack_signing_secret.encode('utf-8') # I had an env variable declared with slack_signing_secret
+
+ my_signature = 'v0=' + hmac.new(
+ signing_secret,
+ sig_basestring,
+ hashlib.sha256
+ ).hexdigest()
+
+ slack_signature = request.headers['x-slack-signature']
+ if hmac.compare_digest(my_signature, slack_signature):
+ return True
+ else:
+ return False
+