add /api endpoint for automated flows (#316)
* add /api endpoint * pass password in request body when using API * flake8 fixed; tests added * flake8 fixed test.py --------- Co-authored-by: Reinoud van Leeuwen <reinoud.van.leeuwen@itcreation.nl>
This commit is contained in:
parent
04f9402e5f
commit
dc321ef79c
3 changed files with 117 additions and 18 deletions
34
README.rst
34
README.rst
|
@ -96,6 +96,40 @@ need to change this.
|
||||||
|
|
||||||
``HOST_OVERRIDE``: (optional) Used to override the base URL if the app is unaware. Useful when running behind reverse proxies like an identity-aware SSO. Example: ``sub.domain.com``
|
``HOST_OVERRIDE``: (optional) Used to override the base URL if the app is unaware. Useful when running behind reverse proxies like an identity-aware SSO. Example: ``sub.domain.com``
|
||||||
|
|
||||||
|
API
|
||||||
|
---
|
||||||
|
|
||||||
|
SnapPass also has a simple API that can be used to create passwords links. The advantage of using the API is that
|
||||||
|
you can create a password and retrieve the link without having to open the web interface. This is useful if you want to
|
||||||
|
embed it in a script or use it in a CI/CD pipeline.
|
||||||
|
|
||||||
|
To create a password, send a POST request to ``/api/set_password`` like so:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$ curl -X POST -H "Content-Type: application/json" -d '{"password": "foobar"}' http://localhost:5000/api/set_password/
|
||||||
|
|
||||||
|
This will return a JSON response with the password link:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"link": "http://127.0.0.1:5000/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D",
|
||||||
|
"ttl":1209600
|
||||||
|
}
|
||||||
|
|
||||||
|
the default TTL is 2 weeks (1209600 seconds), but you can override it by adding a expiration parameter:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$ curl -X POST -H "Content-Type: application/json" -d '{"password": "foobar", "ttl": 3600 }' http://localhost:5000/api/set_password/
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- When using the API, you can specify any ttl, as long as it is lower than the default.
|
||||||
|
- The password is passed in the body of the request rather than in the URL. This is to prevent the password from being logged in the server logs.
|
||||||
|
- Depending on the environment you are running it, you might want to expose the ``/api`` endpoint to your internal network only, and put the web interface behind authentication.
|
||||||
|
|
||||||
Docker
|
Docker
|
||||||
------
|
------
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ URL_PREFIX = os.environ.get('URL_PREFIX', None)
|
||||||
HOST_OVERRIDE = os.environ.get('HOST_OVERRIDE', None)
|
HOST_OVERRIDE = os.environ.get('HOST_OVERRIDE', None)
|
||||||
TOKEN_SEPARATOR = '~'
|
TOKEN_SEPARATOR = '~'
|
||||||
|
|
||||||
|
|
||||||
# Initialize Flask Application
|
# Initialize Flask Application
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
if os.environ.get('DEBUG'):
|
if os.environ.get('DEBUG'):
|
||||||
|
@ -37,6 +36,7 @@ babel = Babel(app, locale_selector=get_locale)
|
||||||
# Initialize Redis
|
# Initialize Redis
|
||||||
if os.environ.get('MOCK_REDIS'):
|
if os.environ.get('MOCK_REDIS'):
|
||||||
from fakeredis import FakeStrictRedis
|
from fakeredis import FakeStrictRedis
|
||||||
|
|
||||||
redis_client = FakeStrictRedis()
|
redis_client = FakeStrictRedis()
|
||||||
elif os.environ.get('REDIS_URL'):
|
elif os.environ.get('REDIS_URL'):
|
||||||
redis_client = redis.StrictRedis.from_url(os.environ.get('REDIS_URL'))
|
redis_client = redis.StrictRedis.from_url(os.environ.get('REDIS_URL'))
|
||||||
|
@ -48,7 +48,10 @@ else:
|
||||||
host=redis_host, port=redis_port, db=redis_db)
|
host=redis_host, port=redis_port, db=redis_db)
|
||||||
REDIS_PREFIX = os.environ.get('REDIS_PREFIX', 'snappass')
|
REDIS_PREFIX = os.environ.get('REDIS_PREFIX', 'snappass')
|
||||||
|
|
||||||
TIME_CONVERSION = {'two weeks': 1209600, 'week': 604800, 'day': 86400, 'hour': 3600}
|
TIME_CONVERSION = {'two weeks': 1209600, 'week': 604800, 'day': 86400,
|
||||||
|
'hour': 3600}
|
||||||
|
DEFAULT_API_TTL = 1209600
|
||||||
|
MAX_TTL = DEFAULT_API_TTL
|
||||||
|
|
||||||
|
|
||||||
def check_redis_alive(fn):
|
def check_redis_alive(fn):
|
||||||
|
@ -63,6 +66,7 @@ def check_redis_alive(fn):
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
else:
|
else:
|
||||||
return abort(500)
|
return abort(500)
|
||||||
|
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
@ -163,6 +167,22 @@ def clean_input():
|
||||||
return TIME_CONVERSION[time_period], request.form['password']
|
return TIME_CONVERSION[time_period], request.form['password']
|
||||||
|
|
||||||
|
|
||||||
|
def set_base_url(req):
|
||||||
|
if NO_SSL:
|
||||||
|
if HOST_OVERRIDE:
|
||||||
|
base_url = f'http://{HOST_OVERRIDE}/'
|
||||||
|
else:
|
||||||
|
base_url = req.url_root
|
||||||
|
else:
|
||||||
|
if HOST_OVERRIDE:
|
||||||
|
base_url = f'https://{HOST_OVERRIDE}/'
|
||||||
|
else:
|
||||||
|
base_url = req.url_root.replace("http://", "https://")
|
||||||
|
if URL_PREFIX:
|
||||||
|
base_url = base_url + URL_PREFIX.strip("/") + "/"
|
||||||
|
return base_url
|
||||||
|
|
||||||
|
|
||||||
@app.route('/', methods=['GET'])
|
@app.route('/', methods=['GET'])
|
||||||
def index():
|
def index():
|
||||||
return render_template('set_password.html')
|
return render_template('set_password.html')
|
||||||
|
@ -170,26 +190,33 @@ def index():
|
||||||
|
|
||||||
@app.route('/', methods=['POST'])
|
@app.route('/', methods=['POST'])
|
||||||
def handle_password():
|
def handle_password():
|
||||||
ttl, password = clean_input()
|
password = request.form.get('password')
|
||||||
token = set_password(password, ttl)
|
ttl = request.form.get('ttl')
|
||||||
|
if clean_input():
|
||||||
if NO_SSL:
|
ttl = TIME_CONVERSION[ttl.lower()]
|
||||||
if HOST_OVERRIDE:
|
token = set_password(password, ttl)
|
||||||
base_url = f'http://{HOST_OVERRIDE}/'
|
base_url = set_base_url(request)
|
||||||
|
link = base_url + quote_plus(token)
|
||||||
|
if request.accept_mimetypes.accept_json and not \
|
||||||
|
request.accept_mimetypes.accept_html:
|
||||||
|
return jsonify(link=link, ttl=ttl)
|
||||||
else:
|
else:
|
||||||
base_url = request.url_root
|
return render_template('confirm.html', password_link=link)
|
||||||
else:
|
else:
|
||||||
if HOST_OVERRIDE:
|
abort(500)
|
||||||
base_url = f'https://{HOST_OVERRIDE}/'
|
|
||||||
else:
|
|
||||||
base_url = request.url_root.replace("http://", "https://")
|
@app.route('/api/set_password/', methods=['POST'])
|
||||||
if URL_PREFIX:
|
def api_handle_password():
|
||||||
base_url = base_url + URL_PREFIX.strip("/") + "/"
|
password = request.json.get('password')
|
||||||
link = base_url + quote_plus(token)
|
ttl = int(request.json.get('ttl', DEFAULT_API_TTL))
|
||||||
if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html:
|
if password and isinstance(ttl, int) and ttl <= MAX_TTL:
|
||||||
|
token = set_password(password, ttl)
|
||||||
|
base_url = set_base_url(request)
|
||||||
|
link = base_url + quote_plus(token)
|
||||||
return jsonify(link=link, ttl=ttl)
|
return jsonify(link=link, ttl=ttl)
|
||||||
else:
|
else:
|
||||||
return render_template('confirm.html', password_link=link)
|
abort(500)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/<password_key>', methods=['GET'])
|
@app.route('/<password_key>', methods=['GET'])
|
||||||
|
|
38
tests.py
38
tests.py
|
@ -163,6 +163,44 @@ class SnapPassRoutesTestCase(TestCase):
|
||||||
frozen_time.move_to("2020-05-22 12:00:00")
|
frozen_time.move_to("2020-05-22 12:00:00")
|
||||||
self.assertIsNone(snappass.get_password(key))
|
self.assertIsNone(snappass.get_password(key))
|
||||||
|
|
||||||
|
def test_set_password_api(self):
|
||||||
|
with freeze_time("2020-05-08 12:00:00") as frozen_time:
|
||||||
|
password = 'my name is my passport. verify me.'
|
||||||
|
rv = self.app.post(
|
||||||
|
'/api/set_password/',
|
||||||
|
headers={'Accept': 'application/json'},
|
||||||
|
json={'password': password, 'ttl': '1209600'},
|
||||||
|
)
|
||||||
|
|
||||||
|
json_content = rv.get_json()
|
||||||
|
key = re.search(r'https://localhost/([^"]+)', json_content['link']).group(1)
|
||||||
|
key = unquote(key)
|
||||||
|
|
||||||
|
frozen_time.move_to("2020-05-22 11:59:59")
|
||||||
|
self.assertEqual(snappass.get_password(key), password)
|
||||||
|
|
||||||
|
frozen_time.move_to("2020-05-22 12:00:00")
|
||||||
|
self.assertIsNone(snappass.get_password(key))
|
||||||
|
|
||||||
|
def test_set_password_api_default_ttl(self):
|
||||||
|
with freeze_time("2020-05-08 12:00:00") as frozen_time:
|
||||||
|
password = 'my name is my passport. verify me.'
|
||||||
|
rv = self.app.post(
|
||||||
|
'/api/set_password/',
|
||||||
|
headers={'Accept': 'application/json'},
|
||||||
|
json={'password': password},
|
||||||
|
)
|
||||||
|
|
||||||
|
json_content = rv.get_json()
|
||||||
|
key = re.search(r'https://localhost/([^"]+)', json_content['link']).group(1)
|
||||||
|
key = unquote(key)
|
||||||
|
|
||||||
|
frozen_time.move_to("2020-05-22 11:59:59")
|
||||||
|
self.assertEqual(snappass.get_password(key), password)
|
||||||
|
|
||||||
|
frozen_time.move_to("2020-05-22 12:00:00")
|
||||||
|
self.assertIsNone(snappass.get_password(key))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
Loading…
Reference in a new issue