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 3 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
80 changes: 54 additions & 26 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 @@ -136,22 +140,39 @@ def empty(value):
return True


def clean_input():
def clean_input(password: str = None, ttl: [str,int]=None):
"""
Make sure we're not getting bad data from the front end,
format data to be machine readable
"""
if empty(request.form.get('password', '')):
if empty(password):
abort(400)

if empty(request.form.get('ttl', '')):
if empty(ttl):
abort(400)
if isinstance(ttl, str):
time_period = ttl.lower()
if time_period not in TIME_CONVERSION:
abort(400)
else:
if ttl < 0 or ttl > MAX_TTL:
abort(400)
return True

time_period = request.form['ttl'].lower()
if time_period not in TIME_CONVERSION:
abort(400)

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'])
Expand All @@ -161,26 +182,33 @@ def index():

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

if NO_SSL:
if HOST_OVERRIDE:
base_url = f'http://{HOST_OVERRIDE}/'
password = request.form.get('password')
ttl = request.form.get('ttl')
if clean_input(password, ttl):
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:
base_url = request.url_root
return render_template('confirm.html', password_link=link)
else:
if HOST_OVERRIDE:
base_url = f'https://{HOST_OVERRIDE}/'
else:
base_url = request.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:
abort(500)


@app.route('/api/set_password/', methods=['POST'])
def api_handle_password():
password = request.json.get('password')
ttl = request.json.get('ttl', DEFAULT_API_TTL)
if clean_input(password, 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
Loading