diff --git a/.dockerignore b/.dockerignore index f788d6d4..5935edca 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,7 +6,6 @@ CONTRIBUTING.rst docker-compose.yml Dockerfile -# requirements.txt tests.py tox.ini diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 00000000..99b41224 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,8 @@ +# Drone CI File! + +kind: template +load: container.jsonnet +data: + repositoryName: josaorg/safepass + releaseName: safepass + buildArgs: diff --git a/.github/codeql-config.yml b/.github/codeql-config.yml new file mode 100644 index 00000000..423a81b7 --- /dev/null +++ b/.github/codeql-config.yml @@ -0,0 +1,8 @@ +name: "CodeQL config" + +paths-ignore: + - tests.py + - 'snappass/static/bootstrap/**' + - 'snappass/static/clipboardjs/**' + - 'snappass/static/fontawesome/**' + - 'snappass/static/jquery/**' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ccf03507..b46812a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,25 +13,25 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.8', '3.9', '3.10'] steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - uses: actions/cache@v3 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-python-${{ matrix.python-version }}-pip-${{ hashFiles('.github/workflows/ci.yml') }} - restore-keys: ${{ runner.os }}-python-${{ matrix.python-version }}-pip - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install tox tox-gh-actions - - name: Lint - if: matrix.python-version == '3.10' - run: tox -e flake8 - - name: Tests - run: tox + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-python-${{ matrix.python-version }}-pip-${{ hashFiles('.github/workflows/ci.yml') }} + restore-keys: ${{ runner.os }}-python-${{ matrix.python-version }}-pip + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox tox-gh-actions + - name: Lint + if: matrix.python-version == '3.10' + run: tox -e flake8 + - name: Tests + run: tox diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..203e66dc --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,45 @@ +name: "CodeQL" + +on: + push: + branches: ["master"] + pull_request: + # The branches below must be a subset of the branches above + branches: ["master"] + # Skip the workflow if PR only contains changes to files matching the following path patterns + paths-ignore: + - tests.py + - "**/*.md" + - "**/*.rst" + - "snappass/static/bootstrap/**" + - "snappass/static/clipboardjs/**" + - "snappass/static/fontawesome/**" + - "snappass/static/jquery/**" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["javascript", "python"] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + config-file: ./.github/codeql-config.yml + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.gitignore b/.gitignore index 6c53d83e..bd8f060d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,7 @@ .project *.rdb junit*xml - -env/ +env/.env* # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -13,7 +12,7 @@ __pycache__/ # Distribution / packaging .Python -env/ +.env.* build/ develop-eggs/ dist/ @@ -50,3 +49,7 @@ htmlcov/ # virtualenv venv/ ENV/ + +# Translation catalogs +*.mo +*.pot diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 68508199..47f4ccaf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,15 +1,20 @@ # .pre-commit-config.yaml -default_stages: [commit] +default_stages: [pre-commit] + repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v5.0.0 hooks: - id: check-merge-conflict - id: check-added-large-files - id: trailing-whitespace - id: end-of-file-fixer - repo: https://github.com/zricethezav/gitleaks - rev: v8.15.0 + rev: v8.24.0 hooks: - id: gitleaks name: detect hardcoded secrets + - repo: https://github.com/jordanopensource/pre-commit-hooks + rev: v0.4.4 + hooks: + - id: run-samplr diff --git a/.samplr.yml b/.samplr.yml new file mode 100644 index 00000000..ec1e460c --- /dev/null +++ b/.samplr.yml @@ -0,0 +1,17 @@ +###################### +# samplr configuration +###################### + +# Want to include a header that indicates this is an autogenerated file? +autogencomments: true + +# regex matches for filenames that you want to include or exclude from +# the samplr generation. Be careful to indent using spaces. +# For a file to be sampled, it has to meet both requirements: be included and not excluded. +excludes: + - \.sample # It's a good idea to exclude all *.sample* files, otherwise it will behave recursively + - \.py$ # Most of the times, the code files won't need samples +includes: + - ^env # Matches files like ./config/setup.yml or ./configuration/script.sh + - \.yml$ # Matches all .yml files in the project + - Dockerfile # Matches all Dockerfile in the project diff --git a/Dockerfile b/Dockerfile index acd500e0..871a0ff9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,13 +8,14 @@ RUN groupadd -r snappass && \ WORKDIR $APP_DIR -COPY ["setup.py", "MANIFEST.in", "README.rst", "AUTHORS.rst", "Makefile", "requirements.txt", "$APP_DIR/"] +COPY ["setup.py", "requirements.txt", "MANIFEST.in", "README.rst", "AUTHORS.rst", "$APP_DIR/"] COPY ["./snappass", "$APP_DIR/snappass"] -RUN apt-get update && \ - apt-get install make && \ - make prod && \ - python setup.py install && \ +RUN pip install -r requirements.txt + +RUN pybabel compile -d snappass/translations + +RUN python setup.py install && \ chown -R snappass $APP_DIR && \ chgrp -R snappass $APP_DIR diff --git a/MANIFEST.in b/MANIFEST.in index ec67e989..e919377d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ include *.rst LICENSE recursive-include snappass/static * recursive-include snappass/templates * +recursive-include snappass/translations * diff --git a/README.rst b/README.rst index 1781f158..251b50fb 100644 --- a/README.rst +++ b/README.rst @@ -47,7 +47,7 @@ Requirements ------------ * `Redis`_ -* Python 3.7+ +* Python 3.8+ .. _Redis: https://redis.io/ @@ -96,6 +96,186 @@ 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`` +``SNAPPASS_BIND_ADDRESS``: (optional) Used to override the default bind address of 0.0.0.0 for flask app Example: ``127.0.0.1`` + +``SNAPPASS_PORT``: (optional) Used to override the default port of 5000 Example: ``6000`` + +APIs +---- + +SnapPass has 2 APIs : +1. A simple API : That can be used to create passwords links, and then share them with users +2. A more REST-y API : Which facilitate programmatic interactions with SnapPass, without having to parse HTML content when retrieving the password + +Simple API +^^^^^^^^^^ + +The advantage of using the simple 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/ + + +REST API +^^^^^^^^ + +The advantage of using the REST API is that you can fully manage the lifecycle of the password stored in SnapPass without having to interact with any web user interface. + +This is useful if you want to embed it in a script, use it in a CI/CD pipeline or share it between multiple client applications. + +Create a password +""""""""""""""""" + +To create a password, send a POST request to ``/api/v2/passwords`` like so: + +:: + + $ curl -X POST -H "Content-Type: application/json" -d '{"password": "foobar"}' http://localhost:5000/api/v2/passwords + +This will return a JSON response with a token and the password link: + +:: + + { + "token": "snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY=", + "links": [{ + "rel": "self", + "href": "http://127.0.0.1:5000/api/v2/passwords/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D", + },{ + "rel": "web-view", + "href": "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/v2/passwords + +If the password is null or empty, and the TTL is larger than the max TTL of the application, the API will return an error like this: + + +Otherwise, the API will return a 404 (Not Found) response like so: + +:: + + { + "invalid-params": [{ + "name": "password", + "reason": "The password is required and should not be null or empty." + }, { + "name": "ttl", + "reason": "The specified TTL is longer than the maximum supported." + }], + "title": "The password and/or the TTL are invalid.", + "type": "https://127.0.0.1:5000/set-password-validation-error" + } + +Check if a password exists +"""""""""""""""""""""""""" + +To check if a password exists, send a HEAD request to ``/api/v2/passwords/``, where ```` is the token of the API response when a password is created (url encoded), or simply use the `self` link: + +:: + + $ curl --head http://localhost:5000/api/v2/passwords/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D + +If : +- the passwork_key is valid +- the password : + - exists, + - has not been read + - is not expired + +Then the API will return a 200 (OK) response like so: + +:: + + HTTP/1.1 200 OK + Server: Werkzeug/3.0.1 Python/3.12.2 + Date: Fri, 29 Mar 2024 22:15:54 GMT + Content-Type: text/html; charset=utf-8 + Content-Length: 0 + Connection: close + +Otherwise, the API will return a 404 (Not Found) response like so: + +:: + + HTTP/1.1 404 NOT FOUND + Server: Werkzeug/3.0.1 Python/3.12.2 + Date: Fri, 29 Mar 2024 22:19:29 GMT + Content-Type: text/html; charset=utf-8 + Content-Length: 0 + Connection: close + + +Read a password +""""""""""""""" + +To read a password, send a GET request to ``/api/v2/passwords/``, where ```` is the token of the API response when a password is created, or simply use the `self` link: + +:: + + $ curl -X GET http://localhost:5000/api/v2/passwords/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D + +If : +- the token is valid +- the password : + - exists + - has not been read + - is not expired + +Then the API will return a 200 (OK) with a JSON response containing the password : + +:: + + { + "password": "foobar" + } + +Otherwise, the API will return a 404 (Not Found) response like so: + +:: + + { + "invalid-params": [{ + "name": "token" + }], + "title": "The password doesn't exist.", + "type": "https://127.0.0.1:5000/get-password-error" + } + +Notes on APIs +^^^^^^^^^^^^^ + +Notes: + +- When using the APIs, 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 ------ diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 00000000..4530a7b9 --- /dev/null +++ b/babel.cfg @@ -0,0 +1,9 @@ +# Update Translations: +# (venv) $ pybabel extract -F babel.cfg -o messages.pot . +# (venv) $ pybabel update -i messages.pot -d snappass/translations +# (venv) $ pybabel compile -d snappass/translations +# Add a new language: +# (venv) $ pybabel extract -F babel.cfg -o messages.pot . +# (venv) $ pybabel init -i messages.pot -d snappass/translations -l +[python: snappass/**.py] +[jinja2: snappass/templates/**.html] diff --git a/dev-requirements.txt b/dev-requirements.txt index e5ecac73..1f11ce37 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,9 @@ -coverage==6.4.1 -fakeredis==1.7.5 -flake8==6.0.0 -freezegun==1.2.1 -pytest==7.1.2 -pytest-cov==3.0.0 -tox==3.25.0 +coverage==7.6.0 +fakeredis==2.25.1 +flake8==7.1.1 +freezegun==1.5.1 +pytest==8.3.2 +pytest-cov==5.0.0 +tox==4.23.0 +bumpversion==0.6.0 +wheel==0.44.0 diff --git a/docker-compose.yml b/docker-compose.yml index 125e7956..684d568c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,13 @@ -version: '2' - services: - safepass: - build: . - image: josaorg/safepass - ports: - - "5000:5000" - stop_signal: SIGINT - environment: - - REDIS_HOST=redis - - NO_SSL=True - depends_on: - - redis - redis: - image: "redis:latest" - # ports: # uncomment for debugging - # - "6379:6379" + safepass: + build: . + image: josaorg/safepass + ports: + - '5000:5000' + stop_signal: SIGINT + env_file: env/.env + depends_on: + - redis + + redis: + image: 'redis:latest' diff --git a/requirements.txt b/requirements.txt index 293aab6b..337ca234 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ -cryptography==38.0.4 -Flask==2.1.2 -itsdangerous==2.1.2 -Jinja2==3.1.2 +cryptography==43.0.1 +Flask==3.0.0 +itsdangerous==2.2.0 +Jinja2==3.1.4 MarkupSafe==2.1.1 -redis==4.3.3 -Werkzeug==2.1.2 +redis==5.1.1 +Werkzeug==3.0.6 +flask-babel diff --git a/setup.cfg b/setup.cfg index da02eac6..632eff56 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,8 +1,8 @@ [bumpversion] -current_version = 1.6.0 +current_version = 1.6.2 commit = True tag = True -files = setup.py snappass/__init__.py +files = setup.py [flake8] show-source = True diff --git a/setup.py b/setup.py index b5410d74..44679724 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='snappass', - version='1.6.0', + version='1.6.2', description="It's like SnapChat... for Passwords.", long_description=(open('README.rst').read() + '\n\n' + open('AUTHORS.rst').read()), @@ -18,7 +18,7 @@ ], }, include_package_data=True, - python_requires='>=3.7, <4', + python_requires='>=3.8, <4', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', @@ -27,7 +27,6 @@ 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', diff --git a/snappass/main.py b/snappass/main.py index 5797e1e9..3f546ef7 100644 --- a/snappass/main.py +++ b/snappass/main.py @@ -5,25 +5,20 @@ import redis from cryptography.fernet import Fernet -from flask import abort, Flask, render_template, request, jsonify +from flask import abort, Flask, render_template, request, jsonify, make_response from redis.exceptions import ConnectionError -from werkzeug.urls import url_quote_plus -from werkzeug.urls import url_unquote_plus +from urllib.parse import quote_plus +from urllib.parse import unquote_plus +from urllib.parse import urljoin from distutils.util import strtobool +# _ is required to get the Jinja templates translated +from flask_babel import Babel, _ # noqa: F401 NO_SSL = bool(strtobool(os.environ.get('NO_SSL', 'False'))) URL_PREFIX = os.environ.get('URL_PREFIX', None) HOST_OVERRIDE = os.environ.get('HOST_OVERRIDE', None) TOKEN_SEPARATOR = '~' -# Get environment variables for matomo -MATOMO_URL = os.environ.get('MATOMO_URL', None) -SITE_ID = os.environ.get('MATOMO_SITE_ID', None) -MATOMO_DATA = { - 'matomo_url': MATOMO_URL, - 'site_id': SITE_ID -} - # Initialize Flask Application app = Flask(__name__) if os.environ.get('DEBUG'): @@ -32,9 +27,18 @@ app.config.update( dict(STATIC_URL=os.environ.get('STATIC_URL', 'static'))) + +# Set up Babel +def get_locale(): + return request.accept_languages.best_match(['en', 'es', 'de', 'nl', 'fr']) + + +babel = Babel(app, locale_selector=get_locale) + # 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')) @@ -46,7 +50,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): @@ -61,6 +68,7 @@ def inner(*args, **kwargs): sys.exit(0) else: return abort(500) + return inner @@ -96,6 +104,37 @@ def parse_token(token): return storage_key, decryption_key +def as_validation_problem(request, problem_type, problem_title, invalid_params): + base_url = set_base_url(request) + + problem = { + "type": base_url + problem_type, + "title": problem_title, + "invalid-params": invalid_params + } + return as_problem_response(problem) + + +def as_not_found_problem(request, problem_type, problem_title, invalid_params): + base_url = set_base_url(request) + + problem = { + "type": base_url + problem_type, + "title": problem_title, + "invalid-params": invalid_params + } + return as_problem_response(problem, 404) + + +def as_problem_response(problem, status_code=None): + if not isinstance(status_code, int) or not status_code: + status_code = 400 + + response = make_response(jsonify(problem), status_code) + response.headers['Content-Type'] = 'application/problem+json' + return response + + @check_redis_alive def set_password(password, ttl): """ @@ -166,47 +205,145 @@ def injectMatomoData(): return MATOMO_DATA -@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 = f"{base_url}retrieve?password_key={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('/api/v2/passwords', methods=['POST']) +def api_v2_set_password(): + password = request.json.get('password') + ttl = int(request.json.get('ttl', DEFAULT_API_TTL)) -@app.route('/retrieve', methods=['GET']) -def preview_password(): - password_key = request.args.to_dict().get("password_key") + invalid_params = [] + + if not password: + invalid_params.append({ + "name": "password", + "reason": "The password is required and should not be null or empty." + }) + + if not isinstance(ttl, int) or ttl > MAX_TTL: + invalid_params.append({ + "name": "ttl", + "reason": "The specified TTL is longer than the maximum supported." + }) + + if len(invalid_params) > 0: + # Return a ProblemDetails expliciting issue with Password and/or TTL + return as_validation_problem( + request, + "set-password-validation-error", + "The password and/or the TTL are invalid.", + invalid_params + ) + + token = set_password(password, ttl) + url_token = quote_plus(token) + base_url = set_base_url(request) + api_link = urljoin(base_url, request.path + "/" + url_token) + web_link = urljoin(base_url, url_token) + response_content = { + "token": token, + "links": [{ + "rel": "self", + "href": api_link + }, { + "rel": "web-view", + "href": web_link + }], + "ttl": ttl + } + return jsonify(response_content) + + +@app.route('/api/v2/passwords/', methods=['HEAD']) +def api_v2_check_password(token): + token = unquote_plus(token) + if not password_exists(token): + # Return NotFound, to indicate that password does not exists (anymore or at all) + return ('', 404) + else: + # Return OK, to indicate that password still exists + return ('', 200) + + +@app.route('/api/v2/passwords/', methods=['GET']) +def api_v2_retrieve_password(token): + token = unquote_plus(token) + password = get_password(token) + if not password: + # Return NotFound, to indicate that password does not exists (anymore or at all) + return as_not_found_problem( + request, + "get-password-error", + "The password doesn't exist.", + [{"name": "token"}] + ) + else: + # Return OK and the password in JSON message + return jsonify(password=password) + + +@app.route('/', methods=['GET']) +def preview_password(password_key): + password_key = unquote_plus(password_key) if not password_exists(password_key): return render_template('expired.html'), 404 return render_template('preview.html') -@app.route('/retrieve', methods=['POST']) -def show_password(): - password_key = request.args.to_dict().get("password_key") +@app.route('/', methods=['POST']) +def show_password(password_key): + password_key = unquote_plus(password_key) password = get_password(password_key) if not password: return render_template('expired.html'), 404 @@ -214,14 +351,16 @@ def show_password(): return render_template('password.html', password=password) -@app.route('/healthcheck', methods=['GET']) -def get_healthcheck(): - return {'result': 'I\'m working👍'} +@app.route('/_/_/health', methods=['GET']) +@check_redis_alive +def health_check(): + return {} @check_redis_alive def main(): - app.run(host='0.0.0.0') + app.run(host=os.environ.get('SNAPPASS_BIND_ADDRESS', '0.0.0.0'), + port=os.environ.get('SNAPPASS_PORT', 5000)) if __name__ == '__main__': diff --git a/snappass/templates/base.html b/snappass/templates/base.html index a0d98e2b..0412cf08 100644 --- a/snappass/templates/base.html +++ b/snappass/templates/base.html @@ -1,9 +1,9 @@ - - + + - SafePass - Share Secrets Safely! - - + {{ _('Snappass - Share Secrets') }} + + diff --git a/snappass/templates/confirm.html b/snappass/templates/confirm.html index 7fe8b295..df284278 100644 --- a/snappass/templates/confirm.html +++ b/snappass/templates/confirm.html @@ -3,15 +3,15 @@ {% block content %}
- -

The secret has been temporarily saved. Send the following URL to your intended recipient.

+ +

{{ _('The secret has been temporarily saved. Send the following URL to your intended recipient.') }}

-
-

The secret has now been permanently deleted from the system, and the URL will no longer work. Refresh this page to verify.

+

{{ _('The secret has now been permanently deleted from the system, and the URL will no longer work. Refresh this page to verify.') }}

{% endblock %} diff --git a/snappass/templates/preview.html b/snappass/templates/preview.html index a0c8114f..7b877b6d 100644 --- a/snappass/templates/preview.html +++ b/snappass/templates/preview.html @@ -4,12 +4,12 @@
-

You can only reveal the secret once!

+

{{ _('You can only reveal the secret once!') }}

- +
diff --git a/snappass/templates/set_password.html b/snappass/templates/set_password.html index e382b961..835ac308 100644 --- a/snappass/templates/set_password.html +++ b/snappass/templates/set_password.html @@ -1,35 +1,55 @@ -{% extends "base.html" %} - -{% block content %} +{% extends "base.html" %} {% block content %}
-
- -
-
-
-
- - -
-
+
+ +
+ +
+
+ + +
+
-
- -
+
+ +
-
- +
+ +
+
- -
-
+
-{% endblock %} - -{% block js %} -{% endblock %} +{% endblock %} {% block js %} {% endblock %} diff --git a/snappass/translations/de/LC_MESSAGES/messages.po b/snappass/translations/de/LC_MESSAGES/messages.po new file mode 100644 index 00000000..3c612da4 --- /dev/null +++ b/snappass/translations/de/LC_MESSAGES/messages.po @@ -0,0 +1,130 @@ +# German translations for SNAPPASS. +# Copyright (C) 2024 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# systeembeheerder , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2024-02-22 11:01+0100\n" +"PO-Revision-Date: 2024-02-16 09:29+0100\n" +"Last-Translator: FULL NAME \n" +"Language: de\n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + +#: snappass/templates/base.html:2 +msgid "en" +msgstr "de" + +#: snappass/templates/base.html:4 +msgid "Snappass - Share Secrets" +msgstr "Snappass - Passwörter teilen" + +#: snappass/templates/base.html:16 +msgid "Share Secret" +msgstr "Passwort teilen" + +#: snappass/templates/confirm.html:6 +msgid "Share Secret Link" +msgstr "Geheimen Link teilen" + +#: snappass/templates/confirm.html:7 +msgid "" +"The secret has been temporarily saved. Send the following URL to your " +"intended recipient." +msgstr "" +"Das Passwort wurde vorübergehend gespeichert. Senden Sie die folgende " +"URL an Ihren gewünschten Empfänger." + +#: snappass/templates/confirm.html:14 snappass/templates/password.html:14 +msgid "Copy to clipboard" +msgstr "In Zwischenablage kopieren" + +#: snappass/templates/expired.html:6 +msgid "Secret not found" +msgstr "Passwort nicht gefunden" + +#: snappass/templates/expired.html:7 +msgid "" +"The requested URL was not found on the server. This could be because this" +" URL never contained a secret, or because it expired or was revealed " +"earlier." +msgstr "" +"Die angeforderte URL wurde auf dem Server nicht gefunden. Dies könnte " +"daran liegen, dass die URL nie ein Passwort enthielt, das " +"Passwort abgelaufen ist oder es bereits offengelegt wurde." + +#: snappass/templates/expired.html:8 +msgid "" +"If this URL was sent to you by someone, make sure to check your spelling " +"or ask the person who sent it to you to send a new secret." +msgstr "" +"Wenn Ihnen diese URL von jemandem gesendet wurde, überprüfen Sie " +"ob Sie die URL korrekt eingegeben haben oder bitten Sie den/die " +"Absender/in, Ihnen ein neues Passwort zu senden." + +#: snappass/templates/password.html:6 snappass/templates/preview.html:7 +msgid "Secret" +msgstr "Passwort" + +#: snappass/templates/password.html:7 +msgid "Save the following secret to a secure location." +msgstr "Speichern Sie das folgende Passwort an einem sicheren Ort." + +#: snappass/templates/password.html:21 +msgid "" +"The secret has now been permanently deleted from the system, and the URL " +"will no longer work. Refresh this page to verify." +msgstr "" +"Das Passwort wurde nun endgültig aus dem System gelöscht, und die URL " +"funktioniert nicht mehr. Aktualisieren Sie diese Seite, um dies zu " +"überprüfen." + +#: snappass/templates/preview.html:9 +msgid "You can only reveal the secret once!" +msgstr "Sie können auf das Passwort nur einmal zugreifen!" + +#: snappass/templates/preview.html:12 +msgid "Reveal secret" +msgstr "Passwort anzeigen" + +#: snappass/templates/set_password.html:6 +msgid "Set Secret" +msgstr "Geheimes Passwort festlegen" + +#: snappass/templates/set_password.html:12 +msgid "" +"SnapPass allows you to share secrets in a secure, ephemeral way. Input a " +"single or multi-line secret, its expiration time, and click Generate URL." +" Share the one-time use URL with your intended recipient." +msgstr "" +"SnapPass ermöglicht es Ihnen, Passwörter auf sichere, kurzlebige Weise zu " +"teilen. Geben Sie ein ein- oder mehrzeiliges Passwort ein, legen Sie die " +"Ablaufzeit fest und klicken Sie auf 'URL generieren'. Teilen Sie die URL " +"für den einmaligen Gebrauch mit dem beabsichtigten Empfänger." + +#: snappass/templates/set_password.html:18 +msgid "Two Weeks" +msgstr "Zwei Wochen" + +#: snappass/templates/set_password.html:19 +msgid "Week" +msgstr "Eine Woche" + +#: snappass/templates/set_password.html:20 +msgid "Day" +msgstr "Ein Tag" + +#: snappass/templates/set_password.html:21 +msgid "Hour" +msgstr "Eine Stunde" + +#: snappass/templates/set_password.html:26 +msgid "Generate URL" +msgstr "URL generieren" diff --git a/snappass/translations/es/LC_MESSAGES/messages.po b/snappass/translations/es/LC_MESSAGES/messages.po new file mode 100644 index 00000000..19cda35d --- /dev/null +++ b/snappass/translations/es/LC_MESSAGES/messages.po @@ -0,0 +1,128 @@ +# Spanish translations for SNAPPASS. +# Copyright (C) 2024 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# systeembeheerder , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2024-02-22 11:01+0100\n" +"PO-Revision-Date: 2024-02-16 09:29+0100\n" +"Last-Translator: FULL NAME \n" +"Language: es\n" +"Language-Team: es \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + +#: snappass/templates/base.html:2 +msgid "en" +msgstr "es" + +#: snappass/templates/base.html:4 +msgid "Snappass - Share Secrets" +msgstr "Snappass - Compartir secretos" + +#: snappass/templates/base.html:16 +msgid "Share Secret" +msgstr "Compartir secretos" + +#: snappass/templates/confirm.html:6 +msgid "Share Secret Link" +msgstr "Compartir enlace secreto" + +#: snappass/templates/confirm.html:7 +msgid "" +"The secret has been temporarily saved. Send the following URL to your " +"intended recipient." +msgstr "" +"El secreto se ha guardado temporalmente. Envíe la siguiente URL a " +"sudestinatario previsto." + +#: snappass/templates/confirm.html:14 snappass/templates/password.html:14 +msgid "Copy to clipboard" +msgstr "Copiar en el portapapeles" + +#: snappass/templates/expired.html:6 +msgid "Secret not found" +msgstr "Secreto no encontrado" + +#: snappass/templates/expired.html:7 +msgid "" +"The requested URL was not found on the server. This could be because this" +" URL never contained a secret, or because it expired or was revealed " +"earlier." +msgstr "" +"La URL solicitada no se encontró en el servidor. Esto podría deberse a " +"estoLa URL nunca contenía un secreto, o porque caducó o fue revelado " +"Antes." + +#: snappass/templates/expired.html:8 +msgid "" +"If this URL was sent to you by someone, make sure to check your spelling " +"or ask the person who sent it to you to send a new secret." +msgstr "" +"Si alguien te envió esta URL, asegúrate de revisar tu ortografíaO pídele " +"a la persona que te lo envió que te envíe un nuevo secreto." + +#: snappass/templates/password.html:6 snappass/templates/preview.html:7 +msgid "Secret" +msgstr "Secreto" + +#: snappass/templates/password.html:7 +msgid "Save the following secret to a secure location." +msgstr "Guarda el siguiente secreto en un lugar seguro." + +#: snappass/templates/password.html:21 +msgid "" +"The secret has now been permanently deleted from the system, and the URL " +"will no longer work. Refresh this page to verify." +msgstr "" +"El secreto ahora se ha eliminado permanentemente del sistema, y la URL Ya" +" no funcionará. Actualiza esta página para verificarlo." + +#: snappass/templates/preview.html:9 +msgid "You can only reveal the secret once!" +msgstr "¡Solo puedes revelar el secreto una vez!" + +#: snappass/templates/preview.html:12 +msgid "Reveal secret" +msgstr "Revelar secreto" + +#: snappass/templates/set_password.html:6 +msgid "Set Secret" +msgstr "Establecer secreto" + +#: snappass/templates/set_password.html:12 +msgid "" +"SnapPass allows you to share secrets in a secure, ephemeral way. Input a " +"single or multi-line secret, its expiration time, and click Generate URL." +" Share the one-time use URL with your intended recipient." +msgstr "" +"SnapPass te permite compartir secretos de forma segura y efímera. " +"Introduzca un secreto de una o varias líneas, su tiempo de caducidad y " +"haga clic en Generar URL.Comparta la URL de un solo uso con el " +"destinatario previsto\"" + +#: snappass/templates/set_password.html:18 +msgid "Two Weeks" +msgstr "Dos semanas" + +#: snappass/templates/set_password.html:19 +msgid "Week" +msgstr "Semana" + +#: snappass/templates/set_password.html:20 +msgid "Day" +msgstr "Día" + +#: snappass/templates/set_password.html:21 +msgid "Hour" +msgstr "Hora" + +#: snappass/templates/set_password.html:26 +msgid "Generate URL" +msgstr "Generar URL" diff --git a/snappass/translations/fr/LC_MESSAGES/messages.po b/snappass/translations/fr/LC_MESSAGES/messages.po new file mode 100644 index 00000000..327772e8 --- /dev/null +++ b/snappass/translations/fr/LC_MESSAGES/messages.po @@ -0,0 +1,126 @@ +# French translations for SNAPPASS. +# Copyright (C) 2024 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# snaaky404 , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: snaaky404@users.noreply.github.com\n" +"POT-Creation-Date: 2024-09-24 09:10+0200\n" +"PO-Revision-Date: 2024-09-24 09:10+0200\n" +"Last-Translator: snaaky404 \n" +"Language: fr\n" +"Language-Team: fr \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.15.0\n" + +#: snappass/templates/base.html:2 +msgid "en" +msgstr "fr" + +#: snappass/templates/base.html:4 +msgid "Snappass - Share Secrets" +msgstr "Snappass - Partagez vos secrets !" + +#: snappass/templates/base.html:16 +msgid "Share Secret" +msgstr "Partager un secret" + +#: snappass/templates/confirm.html:6 +msgid "Share Secret Link" +msgstr "Lien de partage du secret" + +#: snappass/templates/confirm.html:7 +msgid "" +"The secret has been temporarily saved. Send the following URL to your " +"intended recipient." +msgstr "" +"Le secret est sauvegardé temporairement. " +"Envoyez l'URL ci-dessous à votre destinataire." + +#: snappass/templates/confirm.html:14 snappass/templates/password.html:14 +msgid "Copy to clipboard" +msgstr "Copier dans le presse-papier" + +#: snappass/templates/expired.html:6 +msgid "Secret not found" +msgstr "Secret introuvable" + +#: snappass/templates/expired.html:7 +msgid "" +"The requested URL was not found on the server. This could be because this" +" URL never contained a secret, or because it expired or was revealed " +"earlier." +msgstr "" +"L'URL demandée est introuvable, cela peut être dû à " +"plusieurs raisons : une URL inexistante, le secret " +"a expiré ou a déjà été révélé." + +#: snappass/templates/expired.html:8 +msgid "" +"If this URL was sent to you by someone, make sure to check your spelling " +"or ask the person who sent it to you to send a new secret." +msgstr "" +"Si cette URL vous a été envoyé par quelqu'un, assurez-vous que l'URL ne contient " +"pas d'erreur ou contactez votre expéditeur pour obtenir un nouveau secret." + +#: snappass/templates/password.html:6 snappass/templates/preview.html:7 +msgid "Secret" +msgstr "Secret" + +#: snappass/templates/password.html:7 +msgid "Save the following secret to a secure location." +msgstr "Sauvegardez le secret suivant dans un lieu sécurisé." + +#: snappass/templates/password.html:21 +msgid "" +"The secret has now been permanently deleted from the system, and the URL " +"will no longer work. Refresh this page to verify." +msgstr "" +"Le secret a été définitivement supprimé du système, cette URL ne fonctionne plus. " +"Actualisez la page pour vérifier." + +#: snappass/templates/preview.html:9 +msgid "You can only reveal the secret once!" +msgstr "Vous pouvez révéler le secret seulement une fois !" + +#: snappass/templates/preview.html:12 +msgid "Reveal secret" +msgstr "Révéler le secret" + +#: snappass/templates/set_password.html:6 +msgid "Set Secret" +msgstr "Définir le secret" + +#: snappass/templates/set_password.html:12 +msgid "" +"SnapPass allows you to share secrets in a secure, ephemeral way. Input a " +"single or multi-line secret, its expiration time, and click Generate URL." +" Share the one-time use URL with your intended recipient." +msgstr "" +"SnapPass vous permet de partager des secrets de manière éphémère et sécurisée. " +"Renseignez une ou plusieurs lignes, définissez la durée d'expiration et cliquez sur " +"le bouton pour générer l'URL, puis partagez le secret avec votre destinataire." + +#: snappass/templates/set_password.html:18 +msgid "Two Weeks" +msgstr "Deux semaines" + +#: snappass/templates/set_password.html:19 +msgid "Week" +msgstr "Une semaine" + +#: snappass/templates/set_password.html:20 +msgid "Day" +msgstr "Un jour" + +#: snappass/templates/set_password.html:21 +msgid "Hour" +msgstr "Une heure" + +#: snappass/templates/set_password.html:26 +msgid "Generate URL" +msgstr "Générer l'URL" diff --git a/snappass/translations/nl/LC_MESSAGES/messages.po b/snappass/translations/nl/LC_MESSAGES/messages.po new file mode 100644 index 00000000..a7e742e6 --- /dev/null +++ b/snappass/translations/nl/LC_MESSAGES/messages.po @@ -0,0 +1,127 @@ +# Dutch translations for SNAPPASS. +# Copyright (C) 2024 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# systeembeheerder , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2024-02-22 11:01+0100\n" +"PO-Revision-Date: 2024-02-14 21:16+0100\n" +"Last-Translator: \n" +"Language: nl\n" +"Language-Team: nl \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + +#: snappass/templates/base.html:2 +msgid "en" +msgstr "nl" + +#: snappass/templates/base.html:4 +msgid "Snappass - Share Secrets" +msgstr "Snappass - Deel Wachtwoorden" + +#: snappass/templates/base.html:16 +msgid "Share Secret" +msgstr "Stel wachtwoord in" + +#: snappass/templates/confirm.html:6 +msgid "Share Secret Link" +msgstr "Deel wachtwoord link" + +#: snappass/templates/confirm.html:7 +msgid "" +"The secret has been temporarily saved. Send the following URL to your " +"intended recipient." +msgstr "" +"Het wachtwoord is tijdelijk opgeslagen. Deel de volgende URL aan de " +"bedoelde ontvanger." + +#: snappass/templates/confirm.html:14 snappass/templates/password.html:14 +msgid "Copy to clipboard" +msgstr "Kopieer naar het klembord" + +#: snappass/templates/expired.html:6 +msgid "Secret not found" +msgstr "Wachtwoord niet gevonden" + +#: snappass/templates/expired.html:7 +msgid "" +"The requested URL was not found on the server. This could be because this" +" URL never contained a secret, or because it expired or was revealed " +"earlier." +msgstr "" +"De gevraagde URL is niet gevonden op de server. Dat kan omdat deze geen " +"wachtwoord bevat, het is verlopen of het al eerder getoond is." + +#: snappass/templates/expired.html:8 +msgid "" +"If this URL was sent to you by someone, make sure to check your spelling " +"or ask the person who sent it to you to send a new secret." +msgstr "" +"Als deze URL naar u is toegestuurd, controleer de spelling of vraag de " +"verzender om een nieuw wachtwoord link te versturen." + +#: snappass/templates/password.html:6 snappass/templates/preview.html:7 +msgid "Secret" +msgstr "Wachtwoord" + +#: snappass/templates/password.html:7 +msgid "Save the following secret to a secure location." +msgstr "Bewaar het wachtwoord op een veilige plek." + +#: snappass/templates/password.html:21 +msgid "" +"The secret has now been permanently deleted from the system, and the URL " +"will no longer work. Refresh this page to verify." +msgstr "" +"Het wachtwoord is permanent verwijderd van het systeem, de URL werkt niet" +" meer. Herlaad deze pagina ter verificatie" + +#: snappass/templates/preview.html:9 +msgid "You can only reveal the secret once!" +msgstr "Het wachtwoord wordt slechts eenmaal getoond!" + +#: snappass/templates/preview.html:12 +msgid "Reveal secret" +msgstr "Onthul wachtwoord" + +#: snappass/templates/set_password.html:6 +msgid "Set Secret" +msgstr "Stel wachtwoord in" + +#: snappass/templates/set_password.html:12 +msgid "" +"SnapPass allows you to share secrets in a secure, ephemeral way. Input a " +"single or multi-line secret, its expiration time, and click Generate URL." +" Share the one-time use URL with your intended recipient." +msgstr "" +"We stellen je in staat om wachtwoorden op een veilige, tijdelijke manier " +"te delen. Voer een enkel- of meerregelig wachtwoord in, stel de vervaltijd" +" in, en klik op 'URL genereren'. Deel de eenmalig te gebruiken URL met de" +" beoogde ontvanger." + +#: snappass/templates/set_password.html:18 +msgid "Two Weeks" +msgstr "Twee weken" + +#: snappass/templates/set_password.html:19 +msgid "Week" +msgstr "Week" + +#: snappass/templates/set_password.html:20 +msgid "Day" +msgstr "Dag" + +#: snappass/templates/set_password.html:21 +msgid "Hour" +msgstr "Uur" + +#: snappass/templates/set_password.html:26 +msgid "Generate URL" +msgstr "URL genereren" diff --git a/tests.py b/tests.py index 1c385f91..b4b089e9 100644 --- a/tests.py +++ b/tests.py @@ -4,6 +4,7 @@ import uuid from unittest import TestCase from unittest import mock +from urllib.parse import quote from urllib.parse import unquote from cryptography.fernet import Fernet @@ -106,6 +107,11 @@ def setUp(self): snappass.app.config['TESTING'] = True self.app = snappass.app.test_client() + def test_health_check(self): + response = self.app.get('/_/_/health') + self.assertEqual('200 OK', response.status) + self.assertEqual('{}', response.get_data(as_text=True).strip()) + def test_preview_password(self): password = "I like novelty kitten statues!" key = snappass.set_password(password, 30) @@ -158,6 +164,194 @@ 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)) + + def test_set_password_api_v2(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/v2/passwords', + headers={'Accept': 'application/json'}, + json={'password': password, 'ttl': '1209600'}, + ) + + json_content = rv.get_json() + key = unquote(json_content['token']) + + 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_v2_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/v2/passwords', + headers={'Accept': 'application/json'}, + json={'password': password}, + ) + + json_content = rv.get_json() + key = unquote(json_content['token']) + + 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_v2_no_password(self): + rv = self.app.post( + '/api/v2/passwords', + headers={'Accept': 'application/json'}, + json={'password': ''}, + ) + + self.assertEqual(rv.status_code, 400) + + json_content = rv.get_json() + invalid_params = json_content['invalid-params'] + self.assertEqual(len(invalid_params), 1) + bad_password = invalid_params[0] + self.assertEqual(bad_password['name'], 'password') + + def test_set_password_api_v2_too_big_ttl(self): + password = 'my name is my passport. verify me.' + rv = self.app.post( + '/api/v2/passwords', + headers={'Accept': 'application/json'}, + json={'password': password, 'ttl': '1209600000'}, + ) + + self.assertEqual(rv.status_code, 400) + + json_content = rv.get_json() + invalid_params = json_content['invalid-params'] + self.assertEqual(len(invalid_params), 1) + bad_ttl = invalid_params[0] + self.assertEqual(bad_ttl['name'], 'ttl') + + def test_set_password_api_v2_no_password_and_too_big_ttl(self): + rv = self.app.post( + '/api/v2/passwords', + headers={'Accept': 'application/json'}, + json={'password': '', 'ttl': '1209600000'}, + ) + + self.assertEqual(rv.status_code, 400) + + json_content = rv.get_json() + invalid_params = json_content['invalid-params'] + self.assertEqual(len(invalid_params), 2) + bad_password = invalid_params[0] + self.assertEqual(bad_password['name'], 'password') + bad_ttl = invalid_params[1] + self.assertEqual(bad_ttl['name'], 'ttl') + + def test_check_password_api_v2(self): + password = 'my name is my passport. verify me.' + rv = self.app.post( + '/api/v2/passwords', + headers={'Accept': 'application/json'}, + json={'password': password}, + ) + + json_content = rv.get_json() + key = unquote(json_content['token']) + + rvc = self.app.head('/api/v2/passwords/' + quote(key)) + self.assertEqual(rvc.status_code, 200) + + def test_check_password_api_v2_bad_keys(self): + password = 'my name is my passport. verify me.' + rv = self.app.post( + '/api/v2/passwords', + headers={'Accept': 'application/json'}, + json={'password': password}, + ) + + json_content = rv.get_json() + key = unquote(json_content['token']) + + rvc = self.app.head('/api/v2/passwords/' + quote(key[::-1])) + self.assertEqual(rvc.status_code, 404) + + def test_retrieve_password_api_v2(self): + password = 'my name is my passport. verify me.' + rv = self.app.post( + '/api/v2/passwords', + headers={'Accept': 'application/json'}, + json={'password': password}, + ) + + json_content = rv.get_json() + key = unquote(json_content['token']) + + rvc = self.app.get('/api/v2/passwords/' + quote(key)) + self.assertEqual(rv.status_code, 200) + + json_content_retrieved = rvc.get_json() + retrieved_password = json_content_retrieved['password'] + self.assertEqual(retrieved_password, password) + + def test_retrieve_password_api_v2_bad_keys(self): + password = 'my name is my passport. verify me.' + rv = self.app.post( + '/api/v2/passwords', + headers={'Accept': 'application/json'}, + json={'password': password}, + ) + + json_content = rv.get_json() + key = unquote(json_content['token']) + + rvc = self.app.get('/api/v2/passwords/' + quote(key[::-1])) + self.assertEqual(rvc.status_code, 404) + + json_content_retrieved = rvc.get_json() + invalid_params = json_content_retrieved['invalid-params'] + self.assertEqual(len(invalid_params), 1) + bad_token = invalid_params[0] + self.assertEqual(bad_token['name'], 'token') + if __name__ == '__main__': unittest.main() diff --git a/tox.ini b/tox.ini index 47828430..09d190a8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37, py38, py39, py310, flake8 +envlist = py38, py39, py310, flake8 [testenv] setenv =