def test_setup_db_erase_all(app, engine, capsys, erase): app.echo = Echo(verbosity=1) # GIVEN a database with records app.cli_cmd = "init-database" setup_db(app) init_db_tables(app, drop_data=True, add_defaults=True) KEY = "linotp.foobar" item = Config(Key=KEY, Value="123", Type="int", Description="test item") db.session.add(item) db.session.commit() assert db.session.query(Config).filter_by(Key=KEY).count() == 1 db.session.remove() # WHEN I invoke `setup_db` setup_db(app) init_db_tables(app, drop_data=erase, add_defaults=False) if erase: # Additional record should have disappeared assert db.session.query(Config).filter_by(Key=KEY).count() == 0 else: # Additional record should still be there assert db.session.query(Config).filter_by(Key=KEY).count() == 1 item = db.session.query(Config).filter_by(Key=KEY).first() db.session.delete(item) db.session.commit()
def init_db_command(erase_all_data): """ Create new tables The database is initialized and optionally data is cleared. """ if erase_all_data: info = 'Recreating database' else: info = 'Creating database' current_app.echo(info, v=1) try: # Even though we skip initialising the database when doing # `linotp init …`, at this point we do need a database engine # after all. current_app.cli_cmd = 'init-database' # anything but `init` setup_db(current_app) init_db_tables(current_app, erase_all_data) except Exception as exx: current_app.echo(f'Failed to create database: {exx!s}') raise sys.exit(1) current_app.echo('Database created', v=1)
def base_app(tmp_path, request, sqlalchemy_uri, key_directory): """ App instance without context Creates and returns a bare app. If you wish an app with an initialised application context, use the `app` fixture instead """ db_fd, db_path = None, None try: # ------------------------------------------------------------------ -- # if sqlalchemy_uri is the fallback, establish a temp file if sqlalchemy_uri == "sqlite:///{}": db_fd, db_path = tempfile.mkstemp() sqlalchemy_uri = sqlalchemy_uri.format(db_path) # ------------------------------------------------------------------ -- # Skip test if incompatible with sqlite if sqlalchemy_uri.startswith("sqlite:"): if request.node.get_closest_marker("exclude_sqlite"): pytest.skip("non sqlite database required for test") # ------------------------------------------------------------------ -- # create the app with common test config base_app_config = dict( ENV="testing", # doesn't make a huge difference for us TESTING=True, DATABASE_URI=sqlalchemy_uri, AUDIT_DATABASE_URI="SHARED", SQLALCHEMY_TRACK_MODIFICATIONS=False, ROOT_DIR=tmp_path, CACHE_DIR=tmp_path / "cache", DATA_DIR=tmp_path / "data", LOG_FILE_DIR=tmp_path / "logs", AUDIT_PUBLIC_KEY_FILE=key_directory / "audit-public.pem", AUDIT_PRIVATE_KEY_FILE=key_directory / "audit-private.pem", SECRET_FILE=key_directory / "encKey", LOGGING_LEVEL="DEBUG", LOGGING_CONSOLE_LEVEL="DEBUG", DISABLE_CONTROLLERS="", ) config = request.node.get_closest_marker("app_config") if config is not None: base_app_config.update(config.args[0]) os.environ["LINOTP_CFG"] = "" # Pre-generate the important directories for key in ("CACHE_DIR", "DATA_DIR", "LOG_FILE_DIR"): os.makedirs(base_app_config[key], mode=0o770, exist_ok=True) # ----------------------------------------------------------------------- # Fake running `linotp init enc-key` secret_file = base_app_config["SECRET_FILE"] if not os.path.exists(secret_file): sec_key = 3 * "0123456789abcdef" * 4 create_secret_key(filename=secret_file, data=sec_key) # Fake running `linotp init audit-keys` audit_private_key_file = str(base_app_config["AUDIT_PRIVATE_KEY_FILE"]) if not os.path.exists(audit_private_key_file): create_audit_keys( audit_private_key_file, str(base_app_config["AUDIT_PUBLIC_KEY_FILE"]), ) # ----------------------------------------------------------------------- os.environ["LINOTP_CMD"] = "init-database" app = create_app("testing", base_app_config) # Fake running `linotp init database` with app.app_context(): init_db_tables(app, drop_data=True, add_defaults=True) yield app finally: # ------------------------------------------------------------------ -- # in case of sqlite tempfile fallback, we have to wipe the dishes here if db_fd: os.close(db_fd) if db_path: os.unlink(db_path)
def base_app(tmp_path, request, sqlalchemy_uri, key_directory): """ App instance without context Creates and returns a bare app. If you wish an app with an initialised application context, use the `app` fixture instead """ db_fd, db_path = None, None try: # ------------------------------------------------------------------ -- # if sqlalchemy_uri is the fallback, establish a temp file if sqlalchemy_uri == 'sqlite:///{}': db_fd, db_path = tempfile.mkstemp() sqlalchemy_uri = sqlalchemy_uri.format(db_path) # ------------------------------------------------------------------ -- # Skip test if incompatible with sqlite if sqlalchemy_uri.startswith("sqlite:"): if request.node.get_closest_marker('exclude_sqlite'): pytest.skip("non sqlite database required for test") # ------------------------------------------------------------------ -- # create the app with common test config base_app_config = dict( ENV='testing', # doesn't make a huge difference for us TESTING=True, DATABASE_URI=sqlalchemy_uri, SQLALCHEMY_TRACK_MODIFICATIONS=False, ROOT_DIR=tmp_path, CACHE_DIR=tmp_path / "cache", DATA_DIR=tmp_path / "data", LOGFILE_DIR=tmp_path / "logs", AUDIT_PUBLIC_KEY_FILE=key_directory / "audit-public.pem", AUDIT_PRIVATE_KEY_FILE=key_directory / "audit-private.pem", SECRET_FILE=key_directory / "encKey", LOGGING_LEVEL="DEBUG", LOGGING_CONSOLE_LEVEL="DEBUG", ) config = request.node.get_closest_marker("app_config") if config is not None: base_app_config.update(config.args[0]) os.environ["LINOTP_CFG"] = "" # Pre-generate the important directories for key in ('CACHE_DIR', 'DATA_DIR', 'LOGFILE_DIR'): os.makedirs(base_app_config[key], mode=0o770, exist_ok=True) # Create secrete key / audit key if necessary. with mock.patch('linotp.cli.init_cmd.current_app') as mock_app: # The cli commands use current_app.echo to display messages, but this # fails here because there no context yet. So we temporary route # echo to plain print. mock_app.echo = Echo() # Fake running `linotp init enc-key` secret_file = base_app_config['SECRET_FILE'] if not os.path.exists(secret_file): create_secret_key(filename=secret_file) # Fake running `linotp init audit-keys` audit_private_key_file = str( base_app_config['AUDIT_PRIVATE_KEY_FILE']) if not os.path.exists(audit_private_key_file): create_audit_keys( audit_private_key_file, str(base_app_config['AUDIT_PUBLIC_KEY_FILE'])) os.environ["LINOTP_CMD"] = "init-database" app = create_app('testing', base_app_config) # Fake running `linotp init database` with app.app_context(): init_db_tables(app, drop_data=False, add_defaults=True) yield app finally: # ------------------------------------------------------------------ -- # in case of sqlite tempfile fallback, we have to wipe the dishes here if db_fd: os.close(db_fd) if db_path: os.unlink(db_path)
def test_padding_migration(app, base_app, engine): """Check that the padding migration has changed 0. create an empty database 1. set the security module to the old padding, and 2. set the db schema version to lower than the padding migration version and generate an encrypted value with the old padding and store both entries 3. restore the original padding function 4. run the db_init, which now re-encryptes the values the stored encrypted values must now be different, while the decrypted values should be the same """ from linotp.lib.security.default import DefaultSecurityModule class MockSecurityModule(DefaultSecurityModule): @staticmethod def old_padd_data(input_data): data = b"\x01\x02" padding = (16 - len(input_data + data) % 16) % 16 return input_data + data + padding * b"\0" app.echo = Echo(verbosity=1) # we need a base_app context which contains the security provider that is # needed for the re-encryption during the 3.1.0.0 migration with base_app.app_context(): # GIVEN a database with records app.cli_cmd = "init-database" # 0. drop all data and add defaults setup_db(app) init_db_tables(app, drop_data=True, add_defaults=False) # 1. set the security module to the old padding sec_provider = base_app.security_provider sec_module = sec_provider.security_modules[sec_provider.activeOne] sec_module.padd_data = MockSecurityModule.old_padd_data # 2. # set the db schema version to lower than the padding migration # version and generate an encrypted value with the old padding # and store both entries db_schema_version = "linotp.sql_data_model_version" db.session.query(Config).filter_by(Key=db_schema_version).delete() item = Config( Key=db_schema_version, Value="3.0.0.0", Type="", Description="db schema version", ) db.session.add(item) item = Config( Key="linotp.Config", Value="2021-08-25 11:26:13.101147", Type="", Description="db config change time stamp", ) db.session.add(item) value = "Test123Test123Test123Test123Test123" enc_value = sec_module.encryptPassword(value.encode("utf-8")) enc_data_key = "linotp.padding_migration_test_password" enc_item = Config( Key=enc_data_key, Value=enc_value, Type="encrypted_data", Description="migration test password", ) db.session.add(enc_item) pw_token = Token(serial="new_pw_token") pw_token.LinOtpTokenType = "pw" pw_token.LinOtpKeyIV = binascii.hexlify(b":1:") pw_token.LinOtpKeyEnc = binascii.hexlify( b"$6$XjPTQ1cdb8xFEdnF$m.XoQ//RSPABWGym7o9aPx/.RS1ZySekGBDW7wu" b"TZlCDhEM7nf7aOjp03Erk1UFX2OiOhKqaXBMw0a.o4Sbev.") db.session.add(pw_token) hmac_token = Token(serial="new_hmac_token") hmac_token.LinOtpTokenType = "hmac" crypted_value = sec_module.encryptPin( cryptPin=b"01234567890123456789012345678901") iv, _, enc_key = crypted_value.partition(":") hmac_token.LinOtpKeyIV = iv.encode("utf-8") hmac_token.LinOtpKeyEnc = enc_key.encode("utf-8") db.session.add(hmac_token) db.session.commit() db.session.remove() # 3. restore the original padding function sec_module.padd_data = DefaultSecurityModule.padd_data # 4. run the db_init, which now re-encrypts the values setup_db(app) init_db_tables(app, drop_data=False, add_defaults=False) # verify the expected behavior new_enc = db.session.query(Config).filter_by(Key=enc_data_key).first() assert new_enc.Value != enc_value new_value = sec_module.decryptPassword(new_enc.Value).decode("utf-8") assert new_value == value