removed secrets from the code (reom from chatgpt)

This commit is contained in:
2026-05-14 21:16:51 +02:00
parent ff9056fece
commit 7e64c5b4cf
7 changed files with 125 additions and 63 deletions
+45
View File
@@ -7,3 +7,48 @@ install
2. create subfolder "instance" to store database file 2. create subfolder "instance" to store database file
3. for an initial instance: run python3 initialze_data.py, with the python from the virtual-env 3. for an initial instance: run python3 initialze_data.py, with the python from the virtual-env
4. add the below config to your apache2 enabled site 4. add the below config to your apache2 enabled site
## Secrets and configuration
LAPP does not store runtime secrets in the source code. Configure them with
environment variables in production:
```sh
export LAPP_SECRET_KEY="replace-with-a-long-random-secret"
export LAPP_APPLICATION_KEY="replace-with-another-long-random-secret"
export LAPP_DATABASE_URI="sqlite:///app.db"
```
`LAPP_SECRET_KEY` signs Flask sessions and auto-login cookies. Keep it stable:
changing it logs users out and invalidates existing auto-login cookies.
`LAPP_APPLICATION_KEY` is used by `/api/validate_key`.
`LAPP_DATABASE_URI` is optional. If it is not set, LAPP uses `sqlite:///app.db`.
For local development, if `LAPP_SECRET_KEY` or `LAPP_APPLICATION_KEY` is not set,
the app creates stable random secrets in:
```text
instance/secret_key
instance/application_key
```
The `instance/` directory is ignored by git, so these generated secrets should
not be committed.
## Initial data secrets
`initialize_data.py` creates initial users and groups only when the database has
no users yet. You can provide the initial passwords and group secrets with:
```sh
export LAPP_INITIAL_ADMIN_PASSWORD="replace-me"
export LAPP_INITIAL_ADMIN_GROUP="replace-me"
export LAPP_INITIAL_USER_PASSWORD="replace-me"
export LAPP_INITIAL_USER_GROUP="replace-me"
./venv/bin/python initialize_data.py
```
If these variables are not set, `initialize_data.py` generates random values and
prints them once when it creates the initial data.
+2 -2
View File
@@ -1,4 +1,4 @@
from flask import Blueprint, render_template, redirect, url_for, request from flask import Blueprint, render_template, redirect, url_for, request, current_app
from flask_login import login_required, current_user, login_user from flask_login import login_required, current_user, login_user
from models import db from models import db
from models import User, Item from models import User, Item
@@ -14,7 +14,7 @@ def validate_key():
result['data'] = '' result['data'] = ''
if "key" in request.form: if "key" in request.form:
key = request.form["key"] key = request.form["key"]
if key == 'NogNietNodigHier': if key == current_app.config["APPLICATION_KEY"]:
result['status'] = 1 result['status'] = 1
else: else:
result['message'] = 'Unknown Application-Key' result['message'] = 'Unknown Application-Key'
+15 -44
View File
@@ -1,64 +1,35 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response, current_app
from flask_login import login_user, logout_user, login_required, current_user from flask_login import login_user, logout_user, login_required, current_user
from models import db from models import db
from models import User from models import User
from log import Log from log import Log
import pickle from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer
from cryptography.fernet import Fernet
import datetime
auth_bp = Blueprint("auth", __name__) auth_bp = Blueprint("auth", __name__)
# ===================================================== AUTOLOGIN_MAX_AGE_SECONDS = 7 * 24 * 60 * 60
# SECRET KEY (only for this program) AUTOLOGIN_SALT = "lapp-autologin"
# =====================================================
# Generate once using: Fernet.generate_key()
_SECRET_KEY = b'YFK7QCyTzhyLO4vqrnRxvDAI5uu8mXEYrInEjbRoQgs='
fernet = Fernet(_SECRET_KEY)
# ===================================================== def autologin_serializer():
# ENCRYPT return URLSafeTimedSerializer(current_app.config["SECRET_KEY"], salt=AUTOLOGIN_SALT)
# =====================================================
def encrypt_object(obj) -> bytes:
"""
Encrypt any Python object and return encrypted bytes.
"""
serialized = pickle.dumps(obj)
encrypted = fernet.encrypt(serialized)
return encrypted
# =====================================================
# DECRYPT
# =====================================================
def decrypt_object(encrypted_data: bytes):
"""
Decrypt bytes back into the original Python object.
"""
decrypted = fernet.decrypt(encrypted_data)
obj = pickle.loads(decrypted)
return obj
def create_cookie(userid): def create_cookie(userid):
cookie_data = { cookie_data = {
"user": userid, "user": userid
"datetime": datetime.datetime.now()
} }
return encrypt_object(cookie_data).decode('utf-8') return autologin_serializer().dumps(cookie_data)
def is_cookie_ok_to_autologin(cookie_content): def is_cookie_ok_to_autologin(cookie_content):
user = 0 if not cookie_content:
autologin = False return 0
try: try:
cookie_data = decrypt_object(cookie_content) cookie_data = autologin_serializer().loads(cookie_content,
user = cookie_data["user"] max_age=AUTOLOGIN_MAX_AGE_SECONDS)
dt = cookie_data["datetime"] return int(cookie_data["user"])
lastweek = datetime.datetime.now() - datetime.timedelta(days=7) except (BadSignature, SignatureExpired, KeyError, TypeError, ValueError):
autologin = dt > lastweek
except:
Log.info("Cookie error") Log.info("Cookie error")
if autologin:
return user
return 0 return 0
#. @auth_bp.route("/", methods=["GET", "POST"]) #. @auth_bp.route("/", methods=["GET", "POST"])
+20 -4
View File
@@ -1,25 +1,41 @@
from lapp import create_app from lapp import create_app
import os
import secrets
import sqlalchemy as sa import sqlalchemy as sa
from models import db, User, ListOfItems, Shared, Item, Group from models import db, User, ListOfItems, Shared, Item, Group
def initial_secret(env_name):
return os.environ.get(env_name) or secrets.token_urlsafe(18)
if __name__ == "__main__": if __name__ == "__main__":
app = create_app() app = create_app()
app.app_context().push() app.app_context().push()
query = sa.select(User) query = sa.select(User)
users = db.session.scalars(query).all() users = db.session.scalars(query).all()
if len(users) == 0: if len(users) == 0:
g = Group(secret="This is the group just for the admin") admin_group_secret = initial_secret("LAPP_INITIAL_ADMIN_GROUP")
user_group_secret = initial_secret("LAPP_INITIAL_USER_GROUP")
admin_password = initial_secret("LAPP_INITIAL_ADMIN_PASSWORD")
user_password = initial_secret("LAPP_INITIAL_USER_PASSWORD")
g = Group(secret=admin_group_secret)
db.session.add(g) db.session.add(g)
g = Group(secret="GroupForIgnaceAndLoversThatDoGroceries") g = Group(secret=user_group_secret)
db.session.add(g) db.session.add(g)
admin = User(name="admin", group_id=1, is_admin=True, is_approved=True, is_private=True) admin = User(name="admin", group_id=1, is_admin=True, is_approved=True, is_private=True)
admin.set_password("suy2025") admin.set_password(admin_password)
db.session.add(admin) db.session.add(admin)
user1 = User(name="ignace", group_id=2, is_admin=False, is_approved=True) user1 = User(name="ignace", group_id=2, is_admin=False, is_approved=True)
user1.set_password("xanderj2") user1.set_password(user_password)
db.session.add(user1) db.session.add(user1)
db.session.commit() db.session.commit()
print("Created initial credentials:")
print(f"admin password: {admin_password}")
print(f"admin group secret: {admin_group_secret}")
print(f"ignace password: {user_password}")
print(f"ignace group secret: {user_group_secret}")
lol1 = ListOfItems(owner_user_id = user1.id, name="Supermarkt", is_active=True) lol1 = ListOfItems(owner_user_id = user1.id, name="Supermarkt", is_active=True)
db.session.add(lol1) db.session.add(lol1)
lol2 = ListOfItems(owner_user_id = user1.id, name="Klusjes", is_active=True) lol2 = ListOfItems(owner_user_id = user1.id, name="Klusjes", is_active=True)
+24 -3
View File
@@ -1,3 +1,6 @@
import os
import secrets
from pathlib import Path
from flask import Flask, send_from_directory, url_for from flask import Flask, send_from_directory, url_for
from flask_login import LoginManager from flask_login import LoginManager
from models import db from models import db
@@ -11,10 +14,28 @@ from groups import groups_bp
login_manager = LoginManager() login_manager = LoginManager()
def load_secret(app, env_name, filename):
secret = os.environ.get(env_name)
if secret:
return secret
instance_path = Path(app.instance_path)
instance_path.mkdir(parents=True, exist_ok=True)
secret_path = instance_path / filename
if secret_path.exists():
return secret_path.read_text(encoding="utf-8").strip()
secret = secrets.token_urlsafe(48)
secret_path.write_text(secret, encoding="utf-8")
secret_path.chmod(0o600)
return secret
def create_app(): def create_app():
app = Flask(__name__) app = Flask(__name__, instance_relative_config=True)
app.config["SECRET_KEY"] = "9823740934riurehoiuwerf873487qe78feri" app.config["SECRET_KEY"] = load_secret(app, "LAPP_SECRET_KEY", "secret_key")
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db" app.config["APPLICATION_KEY"] = load_secret(app, "LAPP_APPLICATION_KEY", "application_key")
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("LAPP_DATABASE_URI", "sqlite:///app.db")
db.init_app(app) db.init_app(app)
login_manager.init_app(app) login_manager.init_app(app)
+2 -8
View File
@@ -37,12 +37,7 @@ def edit():
abort(403) abort(403)
newname = request.form["name"].strip() newname = request.form["name"].strip()
double = False if not newname or ListOfItems.name_exists_for_owner(current_user.id, newname, exclude_id=ilist_id):
for d in ListOfItems.query.filter_by(owner_user_id=current_user.id, name=newname).all():
if not d.id == ilist_id:
# there is a duplicate name
double = True
if double:
return render_template("lists_edit.html", user=current_user, return render_template("lists_edit.html", user=current_user,
ilists=current_user.my_owned_lists(with_inactive=True), ilists=current_user.my_owned_lists(with_inactive=True),
users=not_current_user(current_user.id), users=not_current_user(current_user.id),
@@ -76,8 +71,7 @@ def edit():
# must be a new list # must be a new list
newlist = ListOfItems(name=request.form["name"].strip(), owner_user_id=current_user.id) newlist = ListOfItems(name=request.form["name"].strip(), owner_user_id=current_user.id)
# check if name exists # check if name exists
duplicate = ListOfItems.query.filter_by(owner_user_id=current_user.id, name=newlist.name).first() if not newlist.name or ListOfItems.name_exists_for_owner(current_user.id, newlist.name):
if duplicate:
# there is a duplicate name # there is a duplicate name
return render_template("lists_edit.html", user=current_user, return render_template("lists_edit.html", user=current_user,
ilists=current_user.my_owned_lists(with_inactive=True), ilists=current_user.my_owned_lists(with_inactive=True),
+16 -1
View File
@@ -2,6 +2,7 @@ from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime from datetime import datetime
from sqlalchemy import func
db = SQLAlchemy() # Create the extension object db = SQLAlchemy() # Create the extension object
@@ -72,12 +73,26 @@ class User(UserMixin, db.Model):
return 0 return 0
class ListOfItems(db.Model): class ListOfItems(db.Model):
__table_args__ = (
db.UniqueConstraint("owner_user_id", "name", name="uq_list_owner_name"),
)
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
updated_at = db.Column(db.TIMESTAMP, default=datetime.now(), onupdate=datetime.now(), nullable=False) updated_at = db.Column(db.TIMESTAMP, default=datetime.now(), onupdate=datetime.now(), nullable=False)
owner_user_id = db.Column(db.Integer, nullable=False) owner_user_id = db.Column(db.Integer, nullable=False)
name = db.Column(db.String(40), unique=True, nullable=False) name = db.Column(db.String(40), nullable=False)
is_active = db.Column(db.Boolean, default=False) is_active = db.Column(db.Boolean, default=False)
@classmethod
def name_exists_for_owner(cls, owner_user_id, name, exclude_id=None):
query = cls.query.filter(
cls.owner_user_id == owner_user_id,
func.lower(cls.name) == name.strip().lower(),
)
if exclude_id is not None:
query = query.filter(cls.id != exclude_id)
return query.first() is not None
def as_dict(self): def as_dict(self):
result = {} result = {}
for p in ['id','name','owner_user_id','is_active']: for p in ['id','name','owner_user_id','is_active']: