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 1 commit
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
30 changes: 30 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,36 @@ 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" http://localhost:5000/api/set_password/my_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" http://localhost:5000/api/set_password/my_password/week

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
72 changes: 47 additions & 25 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,8 @@
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}


def check_redis_alive(fn):
Expand All @@ -54,6 +55,7 @@ def inner(*args, **kwargs):
sys.exit(0)
else:
return abort(500)

return inner


Expand Down Expand Up @@ -136,51 +138,71 @@ def empty(value):
return True


def clean_input():
def clean_input(password=None, ttl=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)

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

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(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:
return render_template('confirm.html', password_link=link)
else:
abort(500)


@app.route('/api/set_password/<string:password>', methods=['POST'])
@app.route('/api/set_password/<string:password>/<string:ttl>', methods=['POST'])
def api_handle_password(password: str, ttl: str = 'two weeks'):
yurushao marked this conversation as resolved.
Show resolved Hide resolved
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)
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