removed secrets from the code (reom from chatgpt)
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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']:
|
||||||
|
|||||||
Reference in New Issue
Block a user