Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add /api endpoint for automated flows #316

Merged
merged 5 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``

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

Expand Down
61 changes: 44 additions & 17 deletions snappass/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
HOST_OVERRIDE = os.environ.get('HOST_OVERRIDE', None)
TOKEN_SEPARATOR = '~'


# Initialize Flask Application
app = Flask(__name__)
if os.environ.get('DEBUG'):
Expand All @@ -28,6 +27,7 @@
# Initialize Redis
if os.environ.get('MOCK_REDIS'):
from fakeredis import FakeStrictRedis

redis_client = FakeStrictRedis()
elif os.environ.get('REDIS_URL'):
redis_client = redis.StrictRedis.from_url(os.environ.get('REDIS_URL'))
Expand All @@ -39,7 +39,10 @@
host=redis_host, port=redis_port, db=redis_db)
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):
Expand All @@ -54,6 +57,7 @@ def inner(*args, **kwargs):
sys.exit(0)
else:
return abort(500)

return inner


Expand Down Expand Up @@ -154,33 +158,56 @@ def clean_input():
return TIME_CONVERSION[time_period], request.form['password']


@app.route('/', methods=['GET'])
def index():
return render_template('set_password.html')


@app.route('/', methods=['POST'])
def handle_password():
ttl, password = clean_input()
token = set_password(password, ttl)

def set_base_url(req):
if NO_SSL:
if HOST_OVERRIDE:
base_url = f'http://{HOST_OVERRIDE}/'
else:
base_url = request.url_root
base_url = req.url_root
else:
if HOST_OVERRIDE:
base_url = f'https://{HOST_OVERRIDE}/'
else:
base_url = request.url_root.replace("http://", "https://")
base_url = req.url_root.replace("http://", "https://")
if URL_PREFIX:
base_url = base_url + URL_PREFIX.strip("/") + "/"
link = base_url + quote_plus(token)
if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html:
return base_url


@app.route('/', methods=['GET'])
def index():
return render_template('set_password.html')


@app.route('/', methods=['POST'])
def handle_password():
password = request.form.get('password')
ttl = request.form.get('ttl')
if clean_input():
ttl = TIME_CONVERSION[ttl.lower()]
token = set_password(password, ttl)
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:
return render_template('confirm.html', password_link=link)
else:
abort(500)


@app.route('/api/set_password/', methods=['POST'])
def api_handle_password():
password = request.json.get('password')
ttl = int(request.json.get('ttl', DEFAULT_API_TTL))
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)
else:
return render_template('confirm.html', password_link=link)
abort(500)


@app.route('/<password_key>', methods=['GET'])
Expand Down
38 changes: 38 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,44 @@ def test_set_password_json(self):
frozen_time.move_to("2020-05-22 12:00:00")
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__':
unittest.main()