From 838fc136480e6aa652992346fc1337234a2ecda6 Mon Sep 17 00:00:00 2001 From: Jona Abdinghoff Date: Fri, 9 Apr 2021 19:37:49 +0200 Subject: [PATCH] v0.9.0 (#62) * backport changes from api-consistency branch --- .coveragerc | 1 - .github/workflows/tests.yml | 12 +- .travis.yml | 4 +- README.md | 10 +- alembic.ini | 71 +++++ alembic/README | 0 alembic/env.py | 90 ++++++ alembic/script.py.mako | 24 ++ .../140a25d5f185_create_tokens_table.py | 69 +++++ config.sample.yaml | 12 +- matrix_registration/__init__.py | 2 +- matrix_registration/api.py | 105 ++++--- matrix_registration/app.py | 15 +- matrix_registration/templates/register.html | 8 +- matrix_registration/tokens.py | 111 +++++-- matrix_registration/translation.py | 2 +- setup.py | 13 +- shell.nix | 1 + tests/test_registration.py | 288 +++++++++--------- tox.ini | 2 +- 20 files changed, 593 insertions(+), 247 deletions(-) create mode 100644 alembic.ini create mode 100644 alembic/README create mode 100644 alembic/env.py create mode 100644 alembic/script.py.mako create mode 100644 alembic/versions/140a25d5f185_create_tokens_table.py diff --git a/.coveragerc b/.coveragerc index b0ac4540..cb141192 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,2 @@ [run] source = matrix_registration -omit = matrix_registration/__main__.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7cb3697d..289ef8f6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,12 @@ name: Tests -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: + branches: + - master jobs: build: @@ -9,7 +15,7 @@ jobs: strategy: matrix: - python-version: [ '3.6', '3.7', '3.8', '3.9' ] + python-version: [ '3.7', '3.8', '3.9' ] name: Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v2 @@ -26,7 +32,7 @@ jobs: # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + flake8 matrix_registration --per-file-ignores="__init__.py:F401" --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with parameterized unit tests 🚨 run: | python setup.py test diff --git a/.travis.yml b/.travis.yml index f4c74e46..35a54a31 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,15 @@ language: python matrix: include: - - python: 3.6 - python: 3.7 dist: focal sudo: true - python: 3.8 dist: focal sudo: true + - python: 3.9 + dist: focal + sudo: true install: - pip install tox-travis - pip install coveralls diff --git a/README.md b/README.md index a00c5604..b86468bf 100644 --- a/README.md +++ b/README.md @@ -100,16 +100,12 @@ The html page looks for the query paramater `token` and sets the token input fie If you already have a website and want to use your own register page, the [wiki](https://github.com/ZerataX/matrix-registration/wiki/reverse-proxy#advanced) describes a more advanced nginx setup. +### bot -### Troubleshooting +if you're looking for a bot to interface with matrix-registration and manage your tokens, take a look at: -#### SQLAlchemy complains that a value isn't in a DateTime value +[maubot-invite](https://github.com/williamkray/maubot-invite) -Before #17 introduced SQLAlchemy support the sqlite database incorrectly stored the expire dates, to fix this you have to manually run: -```sql -update tokens set ex_date=null where ex_date='None'; -``` -on your database once, or just delete your current database. ### Similar projects diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 00000000..921aaf17 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,71 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# timezone to use when rendering the date +# within the migration file as well as the filename. +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +#truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat alembic/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/README b/alembic/README new file mode 100644 index 00000000..e69de29b diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 00000000..7316abc0 --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,90 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +import sys +from os import getcwd +from os.path import abspath, dirname + +sys.path.insert(0, dirname(dirname(abspath(__file__)))) + +from matrix_registration import config as mr_config + + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) + +# load matrix-registration config and set db path for alembic +config_path = context.get_x_argument(as_dictionary=True).get("config") or "config.yaml" +mr_config.config = mr_config.Config(config_path) +config.set_main_option("sqlalchemy.url", mr_config.config.db.replace("{cwd}", f"{getcwd()}/")) + +# add your model's MetaData object here +# for 'autogenerate' support +# target_metadata = mymodel.Base.metadata +target_metadata = None + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 00000000..2c015630 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/140a25d5f185_create_tokens_table.py b/alembic/versions/140a25d5f185_create_tokens_table.py new file mode 100644 index 00000000..8c57b3ac --- /dev/null +++ b/alembic/versions/140a25d5f185_create_tokens_table.py @@ -0,0 +1,69 @@ +"""create tokens table + +Revision ID: 140a25d5f185 +Revises: +Create Date: 2020-12-12 01:44:28.195736 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy import Table, Column, Integer, String, Boolean, DateTime, ForeignKey +from sqlalchemy.engine.reflection import Inspector +from flask_sqlalchemy import SQLAlchemy + + +# revision identifiers, used by Alembic. +revision = '140a25d5f185' +down_revision = None +branch_labels = None +depends_on = None + +db = SQLAlchemy() + + +def upgrade(): + conn = op.get_bind() + inspector = Inspector.from_engine(conn) + tables = inspector.get_table_names() + + if 'ips' not in tables: + op.create_table( + 'ips', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('address', sa.String(255), nullable=True) + ) + + if 'tokens' not in tables: + op.create_table( + 'tokens', + sa.Column('name', String(255), primary_key=True), + sa.Column('expiration_date', DateTime, nullable=True), + sa.Column('max_usage', Integer, default=1), + sa.Column('used', Integer, default=0), + sa.Column('disabled', Boolean, default=False), + sa.Column('ips', Integer, ForeignKey('association.id')) + ) + else: + with op.batch_alter_table('tokens') as batch_op: + batch_op.alter_column('ex_date', new_column_name='expiration_date', nullable=True) + batch_op.alter_column('one_time', new_column_name='max_usage') + + batch_op.add_column( + Column('disabled', Boolean, default=False) + ) + + + if 'association' not in tables: + op.create_table( + 'association', db.Model.metadata, + Column('ips', String, ForeignKey('ips.address'), primary_key=True), + Column('tokens', Integer, ForeignKey('tokens.name'), primary_key=True) + ) + + op.execute("update tokens set expiration_date=null where expiration_date='None'") + + + +def downgrade(): + op.alter_column('tokens', 'expiration_date', new_column_name='ex_date') + op.alter_column('tokens', 'max_usage', new_column_name='one_time') diff --git a/config.sample.yaml b/config.sample.yaml index 5044018b..7b952bf0 100644 --- a/config.sample.yaml +++ b/config.sample.yaml @@ -1,16 +1,18 @@ server_location: 'https://matrix.org' server_name: 'matrix.org' -shared_secret: 'RegistrationSharedSecret' -admin_secret: 'APIAdminPassword' -base_url: '' -riot_instance: 'https://riot.im/app/' +registration_shared_secret: 'RegistrationSharedSecret' # see your synapse's homeserver.yaml +admin_api_shared_secret: 'APIAdminPassword' # to generate tokens via the web api +base_url: '' # e.g. '/element' for https://example.tld/element/register +client_redirect: 'https://app.element.io/#/login' +client_logo: 'static/images/element-logo.png' # use '{cwd}' for current working directory db: 'sqlite:///{cwd}db.sqlite3' host: 'localhost' port: 5000 rate_limit: ["100 per day", "10 per minute"] allow_cors: false +ip_logging: false logging: - disable_existing_loggers: False + disable_existing_loggers: false version: 1 root: level: DEBUG diff --git a/matrix_registration/__init__.py b/matrix_registration/__init__.py index 47595213..02291722 100644 --- a/matrix_registration/__init__.py +++ b/matrix_registration/__init__.py @@ -2,5 +2,5 @@ from . import tokens from . import config -__version__ = '0.8.1.dev3' +__version__ = '0.9.0.dev1' name = 'matrix_registration' diff --git a/matrix_registration/api.py b/matrix_registration/api.py index 3efc2f19..1d155104 100644 --- a/matrix_registration/api.py +++ b/matrix_registration/api.py @@ -3,17 +3,18 @@ from requests import exceptions import re from urllib.parse import urlparse -import gettext +import os # Third-party imports... -from dateutil import parser +from datetime import datetime from flask import ( Blueprint, abort, jsonify, request, make_response, - render_template + render_template, + send_file ) from flask_httpauth import HTTPTokenAuth from werkzeug.exceptions import BadRequest @@ -28,6 +29,7 @@ from .matrix_api import create_account from . import config from . import tokens +from .constants import __location__ from .translation import get_translations @@ -56,7 +58,7 @@ def validate_token(form, token): """ tokens.tokens.load() - if not tokens.tokens.valid(token.data): + if not tokens.tokens.active(token.data): raise validators.ValidationError('Token is invalid') @@ -130,7 +132,7 @@ class RegistrationForm(Form): @auth.verify_token def verify_token(token): - return token == config.config.admin_secret + return token != 'APIAdminPassword' and token == config.config.admin_api_shared_secret @auth.error_handler @@ -142,6 +144,12 @@ def unauthorized(): return make_response(jsonify(resp), 401) +@api.route('/static/replace/images/element-logo.png') +def element_logo(): + return send_file(config.config.client_logo.replace('{cwd}', f'{os.getcwd()}/'), + mimetype='image/jpeg') + + @api.route('/register', methods=['GET', 'POST']) def register(): """ @@ -168,7 +176,7 @@ def register(): account_data = create_account(form.username.data, form.password.data, config.config.server_location, - config.config.shared_secret) + config.config.registration_shared_secret) except exceptions.ConnectionError: logger.error('can not connect to %s' % config.config.server_location, exc_info=True) @@ -191,8 +199,12 @@ def register(): logger.error('failure communicating with HS', exc_info=True) abort(500) + + logger.debug('using token %s' % form.token.data) + ip = request.remote_addr if config.config.ip_logging else False + tokens.tokens.use(form.token.data, ip) + logger.debug('account creation succeded!') - tokens.tokens.use(form.token.data) return jsonify(access_token=account_data['access_token'], home_server=account_data['home_server'], user_id=account_data['user_id'], @@ -218,49 +230,57 @@ def register(): return render_template('register.html', server_name=server_name, pw_length=pw_length, - riot_instance=config.config.riot_instance, + client_redirect=config.config.client_redirect, base_url=config.config.base_url, translations=translations) -@api.route('/token', methods=['GET', 'POST']) +@api.route('/api/version') +@auth.login_required +def version(): + with open(os.path.join(__location__, '__init__.py'), 'r') as file: + version_file = file.read() + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", + version_file, re.M) + resp = {'version': version_match.group(1)} + return make_response(jsonify(resp), 200) + + +@api.route('/api/token', methods=['GET', 'POST']) @auth.login_required def token(): tokens.tokens.load() data = False - one_time = False - ex_date = None + max_usage = False + expiration_date = None if request.method == 'GET': return jsonify(tokens.tokens.toList()) elif request.method == 'POST': - try: - data = request.get_json(force=True) - except BadRequest as e: - # empty request means use default values - if len(request.get_data()) == 0: - data = None - else: - raise e + data = request.get_json() try: if data: - if 'ex_date' in data and data['ex_date'] is not None: - ex_date = parser.parse(data['ex_date']) - if 'one_time' in data: - one_time = data['one_time'] - token = tokens.tokens.new(ex_date=ex_date, - one_time=one_time) + if 'expiration_date' in data and data['expiration_date'] is not None: + expiration_date = datetime.fromisoformat(data['expiration_date']) + if 'max_usage' in data: + max_usage = data['max_usage'] + token = tokens.tokens.new(expiration_date=expiration_date, + max_usage=max_usage) except ValueError: resp = { 'errcode': 'MR_BAD_DATE_FORMAT', - 'error': "date wasn't YYYY-MM-DD format" + 'error': "date wasn't in YYYY-MM-DD format" } return make_response(jsonify(resp), 400) return jsonify(token.toDict()) - abort(400) + resp = { + 'errcode': 'MR_BAD_USER_REQUEST', + 'error': 'malformed request' + } + return make_response(jsonify(resp), 400) -@api.route('/token/', methods=['GET', 'PUT']) +@api.route('/api/token/', methods=['GET', 'PATCH']) @auth.login_required def token_status(token): tokens.tokens.load() @@ -274,21 +294,32 @@ def token_status(token): 'error': 'token does not exist' } return make_response(jsonify(resp), 404) - elif request.method == 'PUT': - data = request.get_json(force=True) + elif request.method == 'PATCH': + try: + data = request.get_json(force=True) + except BadRequest as e: + # empty request means use default values + if len(request.get_data()) == 0: + data = None + else: + raise e if data: - if not data['disable']: + if 'ips' not in data and 'active' not in data and 'name' not in data: + if tokens.tokens.update(token, data): + return jsonify(tokens.tokens.get_token(token).toDict()) + else: resp = { 'errcode': 'MR_BAD_USER_REQUEST', - 'error': 'PUT only allows "disable": true' + 'error': 'you\'re not allowed to change this property' } return make_response(jsonify(resp), 400) - else: - if tokens.tokens.disable(token): - return jsonify(tokens.tokens.get_token(token).toDict()) resp = { 'errcode': 'MR_TOKEN_NOT_FOUND', - 'error': 'token does not exist or is already disabled' + 'error': 'token does not exist' } return make_response(jsonify(resp), 404) - abort(400) + resp = { + 'errcode': 'MR_BAD_USER_REQUEST', + 'error': 'malformed request' + } + return make_response(jsonify(resp), 400) diff --git a/matrix_registration/app.py b/matrix_registration/app.py index ccab8f58..17daab90 100644 --- a/matrix_registration/app.py +++ b/matrix_registration/app.py @@ -59,10 +59,11 @@ def run_server(info): @cli.command("generate", help="generate new token") -@click.option("-o", "--one-time", is_flag=True, help="make token one-time-useable") -@click.option("-e", "--expires", type=click.DateTime(formats=["%Y-%m-%d"]), default=None, help='expire date: in ISO-8601 format (YYYY-MM-DD)') -def generate_token(one_time, expires): - token = tokens.tokens.new(ex_date=expires, one_time=one_time) +@click.option("-m", "--maximum", default=0, help="times token can be used") +@click.option("-e", "--expires", type=click.DateTime(formats=["%Y-%m-%d"]), + default=None, help='expire date: in ISO-8601 format (YYYY-MM-DD)') +def generate_token(maximum, expires): + token = tokens.tokens.new(expiration_date=expires, max_usage=maximum) print(token.name) @@ -76,12 +77,12 @@ def status_token(status, list, disable): print("Token disabled") else: print("Token couldn't be disabled") - if status: + elif status: token = tokens.tokens.get_token(status) if token: - print(f"This token is{' ' if token.valid else ' not '}valid") + print(f"This token is{' ' if token.active() else ' not '}valid") print(json.dumps(token.toDict(), indent=2)) else: print("No token with that name") - if list: + elif list: print(tokens.tokens) diff --git a/matrix_registration/templates/register.html b/matrix_registration/templates/register.html index cdbc7464..e8acd736 100644 --- a/matrix_registration/templates/register.html +++ b/matrix_registration/templates/register.html @@ -16,7 +16,7 @@ {{ translations.server_registration }} - + @@ -88,7 +88,7 @@

{{ translations.click_to_login }}

-

+

{{ translations.choose_client }} https://matrix.org/docs/projects/clients-matrix

@@ -217,7 +217,7 @@

console.log(response) } catch (e) { if (e instanceof SyntaxError) { - showError("{{ translations.internal_error }}") + showError("{{ translations.internal_error }}", "{{ translations.contact }}") return } } @@ -243,7 +243,7 @@

// Define what happens in case of error XHR.addEventListener("error", function (event) { - showError("{{ translations.internal_error }}") + showError("{{ translations.internal_error }}", "{{ translations.contact }}") }) // Set up our request diff --git a/matrix_registration/tokens.py b/matrix_registration/tokens.py index 090de971..58fb2000 100644 --- a/matrix_registration/tokens.py +++ b/matrix_registration/tokens.py @@ -4,6 +4,8 @@ import random from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import exc, Table, Column, Integer, String, Boolean, DateTime, ForeignKey +from sqlalchemy.orm import relationship # Local imports... from .constants import WORD_LIST_PATH @@ -12,6 +14,7 @@ logger = logging.getLogger(__name__) db = SQLAlchemy() +session = db.session def random_readable_string(length=3, wordlist=WORD_LIST_PATH): @@ -23,21 +26,40 @@ def random_readable_string(length=3, wordlist=WORD_LIST_PATH): return string +association_table = Table('association', db.Model.metadata, + Column('ips', String, ForeignKey('ips.address'), primary_key=True), + Column('tokens', Integer, ForeignKey('tokens.name'), primary_key=True)) + + +class IP(db.Model): + __tablename__ = 'ips' + id = Column(Integer, primary_key=True) + address = Column(String(255)) + + def __repr__(self): + return self.address + + class Token(db.Model): __tablename__ = 'tokens' - name = db.Column(db.String(255), primary_key=True) - ex_date = db.Column(db.DateTime, nullable=True) - one_time = db.Column(db.Boolean, default=False) - used = db.Column(db.Integer, default=0) + name = Column(String(255), primary_key=True) + expiration_date = Column(DateTime, nullable=True) + max_usage = Column(Integer, default=1) + used = Column(Integer, default=0) + disabled = Column(Boolean, default=False) + ips = relationship("IP", + secondary=association_table, + lazy='subquery', + backref=db.backref('pages', lazy=True)) def __init__(self, **kwargs): super(Token, self).__init__(**kwargs) if not self.name: self.name = random_readable_string() - if self.used is None: + if not self.used: self.used = 0 - if self.one_time is None: - self.one_time = False + if not self.max_usage: + self.max_usage = 0 def __repr__(self): return self.name @@ -46,29 +68,33 @@ def toDict(self): _token = { 'name': self.name, 'used': self.used, - 'ex_date': str(self.ex_date) if self.ex_date else None, - 'one_time': bool(self.one_time), - 'valid': self.valid() + 'expiration_date': str(self.expiration_date) if self.expiration_date else None, + 'max_usage': self.max_usage, + 'ips': list(map(lambda x: x.address, self.ips)), + 'disabled': bool(self.disabled), + 'active': self.active() } return _token - def valid(self): + def active(self): expired = False - if self.ex_date: - expired = self.ex_date < datetime.now() - used = bool(self.one_time and self.used > 0) + if self.expiration_date: + expired = self.expiration_date < datetime.now() + used = self.max_usage != 0 and self.max_usage <= self.used - return (not expired) and (not used) + return (not expired) and (not used) and (not self.disabled) - def use(self): - if self.valid(): + def use(self, ip_address=False): + if self.active(): self.used += 1 + if ip_address: + self.ips.append(IP(address=ip_address)) return True return False def disable(self): - if self.valid(): - self.ex_date = datetime(1, 1, 1) + if not self.disabled: + self.disabled = True return True return False @@ -92,7 +118,7 @@ def toList(self): return _tokens def load(self): - logger.debug('loading tokens from db...') + logger.debug('loading tokens from ..') self.tokens = {} for token in Token.query.all(): logger.debug(token) @@ -108,38 +134,57 @@ def get_token(self, token_name): return False return token - def valid(self, token_name): - logger.debug('checking if "%s" is valid' % token_name) + def active(self, token_name): + logger.debug('checking if "%s" is active' % token_name) token = self.get_token(token_name) if token: - return token.valid() + return token.active() return False - def use(self, token_name): + def use(self, token_name, ip_address=False): logger.debug('using token: %s' % token_name) token = self.get_token(token_name) if token: - if token.use(): - db.session.commit() + if token.use(ip_address): + try: + session.commit() + except exc.IntegrityError: + logger.warning("User already used this token before!") return True return False + def update(self, token_name, data): + logger.debug('updating token: %s' % token_name) + token = self.get_token(token_name) + if not token: + return False + if 'expiration_date' in data: + token.expiration_date = data['expiration_date'] + if 'max_usage' in data: + token.max_usage = data['max_usage'] + if 'used' in data: + token.used = data['used'] + if 'disabled' in data: + token.disabled = data['disabled'] + session.commit() + return True + def disable(self, token_name): logger.debug('disabling token: %s' % token_name) token = self.get_token(token_name) if token: if token.disable(): - db.session.commit() + session.commit() return True return False - def new(self, ex_date=None, one_time=False): - logger.debug(('creating new token, with options: one_time: {},' + - 'ex_dates: {}').format(one_time, ex_date)) - token = Token(ex_date=ex_date, one_time=one_time) + def new(self, expiration_date=None, max_usage=False): + logger.debug(('creating new token, with options: max_usage: {},' + + 'expiration_dates: {}').format(max_usage, expiration_date)) + token = Token(expiration_date=expiration_date, max_usage=max_usage) self.tokens[token.name] = token - db.session.add(token) - db.session.commit() + session.add(token) + session.commit() return token diff --git a/matrix_registration/translation.py b/matrix_registration/translation.py index a0d5b25d..35c1d2fc 100644 --- a/matrix_registration/translation.py +++ b/matrix_registration/translation.py @@ -25,7 +25,7 @@ def _get_translations(lang='en', replacements={}): translations = yaml.load(stream, Loader=yaml.SafeLoader) interpolated_translations = {} - for key, value in translations['weblate'].items(): + for key, value in translations['weblate'].items(): match = re.search(replace_pattern, value) while match: value = value.replace(match.group(0), str(replacements[match.group('name')])) diff --git a/setup.py b/setup.py index 36cbe5f8..945d47f5 100755 --- a/setup.py +++ b/setup.py @@ -3,6 +3,7 @@ import os import re import setuptools +import glob here = os.path.abspath(os.path.dirname(__file__)) @@ -45,15 +46,15 @@ def find_version(*file_paths): 'static/images/*.jpg', 'static/images/*.png', 'static/images/*.ico']}, - python_requires='~=3.6', + python_requires='~=3.7', install_requires=[ + "alembic>=1.3.2", "appdirs~=1.4.3", "Flask~=1.1", "Flask-SQLAlchemy~=2.4.1", "flask-cors~=3.0.7", "flask-httpauth>=3.3.0", "flask-limiter>=1.1.0", - "python-dateutil~=2.8.1", "PyYAML~=5.1", "requests>=2.22", "SQLAlchemy>=1.3.13,<1.4", @@ -69,17 +70,19 @@ def find_version(*file_paths): "Development Status :: 4 - Beta", "Topic :: Communications :: Chat", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8" + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9" ], entry_points={ 'console_scripts': [ 'matrix-registration=matrix_registration.app:cli' ], }, - test_suite="tests.test_registration", data_files=[ ("config", ["config.sample.yaml"]), + (".", ["alembic.ini"]), + ("alembic", ["alembic/env.py"]), + ("alembic/versions", glob.glob("alembic/versions/*.py")) ] ) diff --git a/shell.nix b/shell.nix index cddf6d4a..da9a00eb 100644 --- a/shell.nix +++ b/shell.nix @@ -13,6 +13,7 @@ in buildPythonPackage { src = ./.; propagatedBuildInputs = [ pkgs.libsndfile + alembic appdirs flask flask-cors diff --git a/tests/test_registration.py b/tests/test_registration.py index 5584e72e..f6d8c381 100644 --- a/tests/test_registration.py +++ b/tests/test_registration.py @@ -19,7 +19,7 @@ # Third-party imports... from parameterized import parameterized -from dateutil import parser +from datetime import datetime from click.testing import CliRunner from flask import Flask @@ -64,14 +64,15 @@ GOOD_CONFIG = { 'server_location': 'https://righths.org', - 'shared_secret': 'coolsharesecret', - 'admin_secret': 'coolpassword', + 'registration_shared_secret': 'coolsharesecret', + 'admin_api_shared_secret': 'coolpassword', 'base_url': '/element', 'db': 'sqlite:///%s/tests/db.sqlite' % (os.getcwd(),), 'port': 5000, 'password': { 'min_length': 8 }, + 'ip_logging': False, 'logging': LOGGING } @@ -82,12 +83,12 @@ BAD_CONFIG2 = dict( # wrong admin secret password -> 401 GOOD_CONFIG.items(), - admin_secret='wrongpassword', + admin_api_shared_secret='wrongpassword', ) BAD_CONFIG3 = dict( # wrong matrix shared password -> 500 GOOD_CONFIG.items(), - shared_secret='wrongsecret', + registration_shared_secret='wrongsecret', ) usernames = [] @@ -152,7 +153,7 @@ def raise_for_status(self): 400) mac = hmac.new( - key=str.encode(GOOD_CONFIG['shared_secret']), + key=str.encode(GOOD_CONFIG['registration_shared_secret']), digestmod=hashlib.sha1, ) @@ -209,34 +210,34 @@ def test_tokens_empty(self): test_tokens = matrix_registration.tokens.Tokens() # no token should exist at this point - self.assertFalse(test_tokens.valid("")) + self.assertFalse(test_tokens.active("")) test_token = test_tokens.new() # no empty token should have been created - self.assertFalse(test_tokens.valid("")) + self.assertFalse(test_tokens.active("")) def test_tokens_disable(self): with self.app.app_context(): test_tokens = matrix_registration.tokens.Tokens() test_token = test_tokens.new() - # new tokens should be valid first, invalid after disabling it - self.assertTrue(test_token.valid()) + # new tokens should be active first, inactive after disabling it + self.assertTrue(test_token.active()) self.assertTrue(test_token.disable()) - self.assertFalse(test_token.valid()) + self.assertFalse(test_token.active()) test_token2 = test_tokens.new() - self.assertTrue(test_tokens.valid(test_token2.name)) + self.assertTrue(test_tokens.active(test_token2.name)) self.assertTrue(test_tokens.disable(test_token2.name)) - self.assertFalse(test_tokens.valid(test_token2.name)) + self.assertFalse(test_tokens.active(test_token2.name)) test_token3 = test_tokens.new() test_token3.use() - self.assertFalse(test_tokens.valid(test_token2.name)) + self.assertFalse(test_tokens.active(test_token2.name)) self.assertFalse(test_tokens.disable(test_token2.name)) - self.assertFalse(test_tokens.valid(test_token2.name)) + self.assertFalse(test_tokens.active(test_token2.name)) def test_tokens_load(self): with self.app.app_context(): @@ -244,9 +245,9 @@ def test_tokens_load(self): test_token = test_tokens.new() test_token2 = test_tokens.new() - test_token3 = test_tokens.new(one_time=True) - test_token4 = test_tokens.new(ex_date=parser.parse("2111-01-01")) - test_token5 = test_tokens.new(ex_date=parser.parse("1999-01-01")) + test_token3 = test_tokens.new(max_usage=True) + test_token4 = test_tokens.new(expiration_date=datetime.fromisoformat("2111-01-01")) + test_token5 = test_tokens.new(expiration_date=datetime.fromisoformat("1999-01-01")) test_tokens.disable(test_token2.name) test_tokens.use(test_token3.name) @@ -254,91 +255,91 @@ def test_tokens_load(self): test_tokens.load() - # token1: valid, unused, no expiration date - # token2: invalid, unused, no expiration date - # token3: used once, one-time, now invalid - # token4: valid, used once, expiration date - # token5: invalid, expiration date + # token1: active, unused, no expiration date + # token2: inactive, unused, no expiration date + # token3: used once, one-time, now inactive + # token4: active, used once, expiration date + # token5: inactive, expiration date self.assertEqual(test_token.name, test_tokens.get_token(test_token.name).name) self.assertEqual(test_token2.name, test_tokens.get_token(test_token2.name).name) - self.assertEqual(test_token2.valid(), - test_tokens.get_token(test_token2.name).valid()) + self.assertEqual(test_token2.active(), + test_tokens.get_token(test_token2.name).active()) self.assertEqual(test_token3.used, test_tokens.get_token(test_token3.name).used) - self.assertEqual(test_token3.valid(), - test_tokens.get_token(test_token3.name).valid()) + self.assertEqual(test_token3.active(), + test_tokens.get_token(test_token3.name).active()) self.assertEqual(test_token4.used, test_tokens.get_token(test_token4.name).used) - self.assertEqual(test_token4.ex_date, - test_tokens.get_token(test_token4.name).ex_date) - self.assertEqual(test_token5.valid(), - test_tokens.get_token(test_token5.name).valid()) + self.assertEqual(test_token4.expiration_date, + test_tokens.get_token(test_token4.name).expiration_date) + self.assertEqual(test_token5.active(), + test_tokens.get_token(test_token5.name).active()) @parameterized.expand([ [None, False], - [parser.parse('2100-01-12'), False], + [datetime.fromisoformat('2100-01-12'), False], [None, True], - [parser.parse('2100-01-12'), True] + [datetime.fromisoformat('2100-01-12'), True] ]) - def test_tokens_new(self, ex_date, one_time): + def test_tokens_new(self, expiration_date, max_usage): with self.app.app_context(): test_tokens = matrix_registration.tokens.Tokens() - test_token = test_tokens.new(ex_date=ex_date, one_time=one_time) + test_token = test_tokens.new(expiration_date=expiration_date, max_usage=max_usage) self.assertIsNotNone(test_token) - if ex_date: - self.assertIsNotNone(test_token.ex_date) + if expiration_date: + self.assertIsNotNone(test_token.expiration_date) else: - self.assertIsNone(test_token.ex_date) - if one_time: - self.assertTrue(test_token.one_time) + self.assertIsNone(test_token.expiration_date) + if max_usage: + self.assertTrue(test_token.max_usage) else: - self.assertFalse(test_token.one_time) - self.assertTrue(test_tokens.valid(test_token.name)) + self.assertFalse(test_token.max_usage) + self.assertTrue(test_tokens.active(test_token.name)) @parameterized.expand([ [None, False, 10, True], - [parser.parse('2100-01-12'), False, 10, True], + [datetime.fromisoformat('2100-01-12'), False, 10, True], [None, True, 1, False], [None, True, 0, True], - [parser.parse('2100-01-12'), True, 1, False], - [parser.parse('2100-01-12'), True, 2, False], - [parser.parse('2100-01-12'), True, 0, True] + [datetime.fromisoformat('2100-01-12'), True, 1, False], + [datetime.fromisoformat('2100-01-12'), True, 2, False], + [datetime.fromisoformat('2100-01-12'), True, 0, True] ]) - def test_tokens_valid_form(self, ex_date, one_time, times_used, valid): + def test_tokens_active_form(self, expiration_date, max_usage, times_used, active): with self.app.app_context(): test_tokens = matrix_registration.tokens.Tokens() - test_token = test_tokens.new(ex_date=ex_date, one_time=one_time) + test_token = test_tokens.new(expiration_date=expiration_date, max_usage=max_usage) for n in range(times_used): test_tokens.use(test_token.name) - if not one_time: + if not max_usage: self.assertEqual(test_token.used, times_used) elif times_used == 0: self.assertEqual(test_token.used, 0) else: self.assertEqual(test_token.used, 1) - self.assertEqual(test_tokens.valid(test_token.name), valid) + self.assertEqual(test_tokens.active(test_token.name), active) @parameterized.expand([ [None, True], - [parser.parse('2100-01-12'), False], - [parser.parse('2200-01-13'), True], + [datetime.fromisoformat('2100-01-12'), False], + [datetime.fromisoformat('2200-01-13'), True], ]) - def test_tokens_valid(self, ex_date, valid): + def test_tokens_active(self, expiration_date, active): with self.app.app_context(): test_tokens = matrix_registration.tokens.Tokens() - test_token = test_tokens.new(ex_date=ex_date) + test_token = test_tokens.new(expiration_date=expiration_date) - self.assertEqual(test_tokens.valid(test_token.name), True) + self.assertEqual(test_tokens.active(test_token.name), True) # date changed to after expiration date with patch('matrix_registration.tokens.datetime') as mock_date: - mock_date.now.return_value = parser.parse('2200-01-12') - self.assertEqual(test_tokens.valid(test_token.name), valid) + mock_date.now.return_value = datetime.fromisoformat('2200-01-12') + self.assertEqual(test_tokens.active(test_token.name), active) @parameterized.expand([ ['DoubleWizardSky'], @@ -402,7 +403,7 @@ def tearDown(self): [''.join(random.choices(string.ascii_uppercase, k=256)), 'test1234', 'test1234', True, 400] ]) - # check form validators + # check form activeators @patch('matrix_registration.matrix_api._get_nonce', side_effect=mocked__get_nonce) @patch('matrix_registration.matrix_api.requests.post', @@ -412,8 +413,8 @@ def test_register(self, username, password, confirm, token, matrix_registration.config.config = Config(GOOD_CONFIG) with self.app.app_context(): matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens() - test_token = matrix_registration.tokens.tokens.new(ex_date=None, - one_time=True) + test_token = matrix_registration.tokens.tokens.new(expiration_date=None, + max_usage=True) # replace matrix with in config set hs domain = urlparse(matrix_registration.config.config.server_location).hostname if username: @@ -441,8 +442,8 @@ def test_register_wrong_hs(self, mock_get, mock_nonce): with self.app.app_context(): matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens() - test_token = matrix_registration.tokens.tokens.new(ex_date=None, - one_time=True) + test_token = matrix_registration.tokens.tokens.new(expiration_date=None, + max_usage=True) rv = self.client.post('/register', data=dict( username='username', password='password', @@ -460,8 +461,8 @@ def test_register_wrong_secret(self, mock_get, mock_nonce): with self.app.app_context(): matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens() - test_token = matrix_registration.tokens.tokens.new(ex_date=None, - one_time=True) + test_token = matrix_registration.tokens.tokens.new(expiration_date=None, + max_usage=True) rv = self.client.post('/register', data=dict( username='username', password='password', @@ -475,31 +476,31 @@ def test_get_tokens(self): with self.app.app_context(): matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens() - test_token = matrix_registration.tokens.tokens.new(ex_date=None, - one_time=True) + test_token = matrix_registration.tokens.tokens.new(expiration_date=None, + max_usage=True) - secret = matrix_registration.config.config.admin_secret + secret = matrix_registration.config.config.admin_api_shared_secret headers = {'Authorization': 'SharedSecret %s' % secret} - rv = self.client.get('/token', headers=headers) + rv = self.client.get('/api/token', headers=headers) self.assertEqual(rv.status_code, 200) token_data = json.loads(rv.data.decode('utf8').replace("'", '"')) - self.assertEqual(token_data[0]['ex_date'], None) - self.assertEqual(token_data[0]['one_time'], True) + self.assertEqual(token_data[0]['expiration_date'], None) + self.assertEqual(token_data[0]['max_usage'], True) def test_error_get_tokens(self): matrix_registration.config.config = Config(BAD_CONFIG2) with self.app.app_context(): matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens() - test_token = matrix_registration.tokens.tokens.new(ex_date=None, - one_time=True) + test_token = matrix_registration.tokens.tokens.new(expiration_date=None, + max_usage=True) - secret = matrix_registration.config.config.admin_secret + secret = matrix_registration.config.config.admin_api_shared_secret matrix_registration.config.config = Config(GOOD_CONFIG) headers = {'Authorization': 'SharedSecret %s' % secret} - rv = self.client.get('/token', headers=headers) + rv = self.client.get('/api/token', headers=headers) self.assertEqual(rv.status_code, 401) token_data = json.loads(rv.data.decode('utf8').replace("'", '"')) @@ -512,26 +513,26 @@ def test_error_get_tokens(self): ['2020-12-24', False, '2020-12-24 00:00:00'], ['2200-05-12', True, '2200-05-12 00:00:00'], ]) - def test_post_token(self, ex_date, one_time, parsed_date): + def test_post_token(self, expiration_date, max_usage, parsed_date): matrix_registration.config.config = Config(GOOD_CONFIG) with self.app.app_context(): matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens() - test_token = matrix_registration.tokens.tokens.new(ex_date=None, - one_time=True) + test_token = matrix_registration.tokens.tokens.new(expiration_date=None, + max_usage=True) - secret = matrix_registration.config.config.admin_secret + secret = matrix_registration.config.config.admin_api_shared_secret headers = {'Authorization': 'SharedSecret %s' % secret} - rv = self.client.post('/token', - data=json.dumps(dict(ex_date=ex_date, - one_time=one_time)), + rv = self.client.post('/api/token', + data=json.dumps(dict(expiration_date=expiration_date, + max_usage=max_usage)), content_type='application/json', headers=headers) self.assertEqual(rv.status_code, 200) token_data = json.loads(rv.data.decode('utf8').replace("'", '"')) - self.assertEqual(token_data['ex_date'], parsed_date) - self.assertEqual(token_data['one_time'], one_time) + self.assertEqual(token_data['expiration_date'], parsed_date) + self.assertEqual(token_data['max_usage'], max_usage) self.assertTrue(token_data['name'] is not None) def test_error_post_token(self): @@ -539,15 +540,15 @@ def test_error_post_token(self): with self.app.app_context(): matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens() - test_token = matrix_registration.tokens.tokens.new(ex_date=None, - one_time=True) + test_token = matrix_registration.tokens.tokens.new(expiration_date=None, + max_usage=True) - secret = matrix_registration.config.config.admin_secret + secret = matrix_registration.config.config.admin_api_shared_secret matrix_registration.config.config = Config(GOOD_CONFIG) headers = {'Authorization': 'SharedSecret %s' % secret} - rv = self.client.post('/token', - data=json.dumps(dict(ex_date='24.12.2020', - one_time=False)), + rv = self.client.post('/api/token', + data=json.dumps(dict(expiration_date='24.12.2020', + max_usage=False)), content_type='application/json', headers=headers) @@ -557,51 +558,51 @@ def test_error_post_token(self): self.assertEqual(token_data['errcode'], 'MR_BAD_SECRET') self.assertEqual(token_data['error'], 'wrong shared secret') - secret = matrix_registration.config.config.admin_secret + secret = matrix_registration.config.config.admin_api_shared_secret headers = {'Authorization': 'SharedSecret %s' % secret} - rv = self.client.post('/token', - data=json.dumps(dict(ex_date='2020-24-12', - one_time=False)), + rv = self.client.post('/api/token', + data=json.dumps(dict(expiration_date='2020-24-12', + max_usage=False)), content_type='application/json', headers=headers) self.assertEqual(rv.status_code, 400) token_data = json.loads(rv.data.decode('utf8')) self.assertEqual(token_data['errcode'], 'MR_BAD_DATE_FORMAT') - self.assertEqual(token_data['error'], "date wasn't YYYY-MM-DD format") + self.assertEqual(token_data['error'], "date wasn't in YYYY-MM-DD format") - def test_put_token(self): + def test_patch_token(self): matrix_registration.config.config = Config(GOOD_CONFIG) with self.app.app_context(): matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens() - test_token = matrix_registration.tokens.tokens.new(one_time=True) + test_token = matrix_registration.tokens.tokens.new(max_usage=True) - secret = matrix_registration.config.config.admin_secret + secret = matrix_registration.config.config.admin_api_shared_secret headers = {'Authorization': 'SharedSecret %s' % secret} - rv = self.client.put('/token/' + test_token.name, - data=json.dumps(dict(disable=True)), + rv = self.client.patch('/api/token/' + test_token.name, + data=json.dumps(dict(disabled=True)), content_type='application/json', headers=headers) self.assertEqual(rv.status_code, 200) token_data = json.loads(rv.data.decode('utf8').replace("'", '"')) - self.assertEqual(token_data['valid'], False) - self.assertEqual(token_data['one_time'], True) + self.assertEqual(token_data['active'], False) + self.assertEqual(token_data['max_usage'], True) self.assertEqual(token_data['name'], test_token.name) - def test_error_put_token(self): + def test_error_patch_token(self): matrix_registration.config.config = Config(BAD_CONFIG2) with self.app.app_context(): matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens() - test_token = matrix_registration.tokens.tokens.new(one_time=True) + test_token = matrix_registration.tokens.tokens.new(max_usage=True) - secret = matrix_registration.config.config.admin_secret + secret = matrix_registration.config.config.admin_api_shared_secret headers = {'Authorization': 'SharedSecret %s' % secret} matrix_registration.config.config = Config(GOOD_CONFIG) - rv = self.client.put('/token/' + test_token.name, - data=json.dumps(dict(disable=True)), + rv = self.client.patch('/api/token/' + test_token.name, + data=json.dumps(dict(disabled=True)), content_type='application/json', headers=headers) @@ -610,62 +611,61 @@ def test_error_put_token(self): self.assertEqual(token_data['errcode'], 'MR_BAD_SECRET') self.assertEqual(token_data['error'], 'wrong shared secret') - secret = matrix_registration.config.config.admin_secret + secret = matrix_registration.config.config.admin_api_shared_secret headers = {'Authorization': 'SharedSecret %s' % secret} - rv = self.client.put('/token/' + test_token.name, - data=json.dumps(dict(disable=False)), + rv = self.client.patch('/api/token/' + test_token.name, + data=json.dumps(dict(active=False)), content_type='application/json', headers=headers) self.assertEqual(rv.status_code, 400) token_data = json.loads(rv.data.decode('utf8')) self.assertEqual(token_data['errcode'], 'MR_BAD_USER_REQUEST') - self.assertEqual(token_data['error'], 'PUT only allows "disable": true') + self.assertEqual(token_data['error'], 'you\'re not allowed to change this property') - rv = self.client.put('/token/' + "nicememe", - data=json.dumps(dict(disable=True)), + rv = self.client.patch('/api/token/' + "nicememe", + data=json.dumps(dict(disabled=True)), content_type='application/json', headers=headers) self.assertEqual(rv.status_code, 404) token_data = json.loads(rv.data.decode('utf8')) self.assertEqual(token_data['errcode'], 'MR_TOKEN_NOT_FOUND') - self.assertEqual(token_data['error'], 'token does not exist or is already disabled') - + self.assertEqual(token_data['error'], 'token does not exist') @parameterized.expand([ [None, True, None], - [parser.isoparse('2020-12-24'), False, '2020-12-24 00:00:00'], - [parser.isoparse('2200-05-12'), True, '2200-05-12 00:00:00'], + [datetime.fromisoformat('2020-12-24'), False, '2020-12-24 00:00:00'], + [datetime.fromisoformat('2200-05-12'), True, '2200-05-12 00:00:00'], ]) - def test_get_token(self, ex_date, one_time, parsed_date): + def test_get_token(self, expiration_date, max_usage, parsed_date): matrix_registration.config.config = Config(BAD_CONFIG2) with self.app.app_context(): matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens() - test_token = matrix_registration.tokens.tokens.new(ex_date=ex_date, - one_time=one_time) + test_token = matrix_registration.tokens.tokens.new(expiration_date=expiration_date, + max_usage=max_usage) - secret = matrix_registration.config.config.admin_secret + secret = matrix_registration.config.config.admin_api_shared_secret headers = {'Authorization': 'SharedSecret %s' % secret} - rv = self.client.get('/token/' + test_token.name, + rv = self.client.get('/api/token/' + test_token.name, content_type='application/json', headers=headers) self.assertEqual(rv.status_code, 200) token_data = json.loads(rv.data.decode('utf8')) - self.assertEqual(token_data['ex_date'], parsed_date) - self.assertEqual(token_data['one_time'], one_time) + self.assertEqual(token_data['expiration_date'], parsed_date) + self.assertEqual(token_data['max_usage'], max_usage) def test_error_get_token(self): matrix_registration.config.config = Config(BAD_CONFIG2) with self.app.app_context(): matrix_registration.tokens.tokens = matrix_registration.tokens.Tokens() - test_token = matrix_registration.tokens.tokens.new(one_time=True) + test_token = matrix_registration.tokens.tokens.new(max_usage=True) - secret = matrix_registration.config.config.admin_secret + secret = matrix_registration.config.config.admin_api_shared_secret headers = {'Authorization': 'SharedSecret %s' % secret} - rv = self.client.get('/token/' + 'nice_meme', + rv = self.client.get('/api/token/' + 'nice_meme', content_type='application/json', headers=headers) @@ -676,11 +676,11 @@ def test_error_get_token(self): matrix_registration.config.config = Config(BAD_CONFIG2) - secret = matrix_registration.config.config.admin_secret + secret = matrix_registration.config.config.admin_api_shared_secret headers = {'Authorization': 'SharedSecret %s' % secret} matrix_registration.config.config = Config(GOOD_CONFIG) - rv = self.client.put('/token/' + test_token.name, - data=json.dumps(dict(disable=True)), + rv = self.client.patch('/api/token/' + test_token.name, + data=json.dumps(dict(disabled=True)), content_type='application/json', headers=headers) @@ -733,31 +733,35 @@ def tearDown(self): def test_create_token(self): runner = create_app().test_cli_runner() - generate = runner.invoke(cli, ['--config-path', self.path, 'generate', '-o']) + generate = runner.invoke(cli, ['--config-path', self.path, 'generate', '-m', 1]) name1 = generate.output.strip() - + status = runner.invoke(cli, ['--config-path', self.path, 'status', '-s', name1]) valid, info_dict_string = status.output.strip().split('\n', 1) self.assertEqual(valid, "This token is valid") comparison_dict = { "name": name1, "used": 0, - "ex_date": None, - "one_time": True, - "valid": True + "expiration_date": None, + "max_usage": 1, + "disabled": False, + "ips": [], + "active": True } self.assertEqual(json.loads(info_dict_string), comparison_dict) runner.invoke(cli, ['--config-path', self.path, 'status', '-d', name1]) status = runner.invoke(cli, ['--config-path', self.path, 'status', '-s', name1]) valid, info_dict_string = status.output.strip().split('\n', 1) - self.assertEqual(valid, "This token is valid") + self.assertEqual(valid, "This token is not valid") comparison_dict = { "name": name1, "used": 0, - "ex_date": "0001-01-01 00:00:00", - "one_time": True, - "valid": False + "expiration_date": None, + "max_usage": 1, + "disabled": True, + "ips": [], + "active": False } self.assertEqual(json.loads(info_dict_string), comparison_dict) @@ -770,9 +774,11 @@ def test_create_token(self): comparison_dict = { "name": name2, "used": 0, - "ex_date": "2220-05-12 00:00:00", - "one_time": False, - "valid": True + "expiration_date": "2220-05-12 00:00:00", + "max_usage": 0, + "disabled": False, + "ips": [], + "active": True } self.assertEqual(json.loads(info_dict_string), comparison_dict) diff --git a/tox.ini b/tox.ini index 7546c05f..625741af 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36,py37,py38 +envlist = py37,py38,p39 [testenv] deps = coveralls commands = coverage erase