diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000000..860a8ad31d2c --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,81 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "mealie-next" ] + pull_request: + branches: [ "mealie-next" ] + schedule: + - cron: '36 9 * * 3' + +jobs: + analyze: + name: Analyze + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners + # Consider using larger runners for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript-typescript', 'python' ] + # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] + # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/alembic/versions/2023-02-14-20.45.41_5ab195a474eb_add_normalized_search_properties.py b/alembic/versions/2023-02-14-20.45.41_5ab195a474eb_add_normalized_search_properties.py index 2cecd66cc196..773aaf5c1212 100644 --- a/alembic/versions/2023-02-14-20.45.41_5ab195a474eb_add_normalized_search_properties.py +++ b/alembic/versions/2023-02-14-20.45.41_5ab195a474eb_add_normalized_search_properties.py @@ -7,12 +7,11 @@ Create Date: 2023-02-14 20:45:41.102571 """ import sqlalchemy as sa from sqlalchemy import orm, select -from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from text_unidecode import unidecode import mealie.db.migration_types from alembic import op - from mealie.db.models._model_utils import GUID # revision identifiers, used by Alembic. @@ -52,30 +51,46 @@ def do_data_migration(): session = orm.Session(bind=bind) recipes = session.execute(select(RecipeModel)).scalars().all() - ingredients = session.execute(select(RecipeIngredient)).scalars().all() for recipe in recipes: if recipe.name is not None: - recipe.name_normalized = unidecode(recipe.name).lower().strip() + session.execute( + sa.text( + f"UPDATE {RecipeModel.__tablename__} SET name_normalized=:name_normalized WHERE id=:id" + ).bindparams(name_normalized=unidecode(recipe.name).lower().strip(), id=recipe.id) + ) if recipe.description is not None: - recipe.description_normalized = unidecode(recipe.description).lower().strip() - session.add(recipe) + session.execute( + sa.text( + f"UPDATE {RecipeModel.__tablename__} SET description_normalized=:description_normalized WHERE id=:id" + ).bindparams(description_normalized=unidecode(recipe.description).lower().strip(), id=recipe.id) + ) + ingredients = session.execute(select(RecipeIngredient)).scalars().all() for ingredient in ingredients: if ingredient.note is not None: - ingredient.note_normalized = unidecode(ingredient.note).lower().strip() + session.execute( + sa.text( + f"UPDATE {RecipeIngredient.__tablename__} SET note_normalized=:note_normalized WHERE id=:id" + ).bindparams(note_normalized=unidecode(ingredient.note).lower().strip(), id=ingredient.id) + ) if ingredient.original_text is not None: - ingredient.original_text_normalized = unidecode(ingredient.original_text).lower().strip() - session.add(ingredient) + session.execute( + sa.text( + f"UPDATE {RecipeIngredient.__tablename__} SET original_text_normalized=:original_text_normalized WHERE id=:id" + ).bindparams( + original_text_normalized=unidecode(ingredient.original_text).lower().strip(), id=ingredient.id + ) + ) session.commit() def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - # Set column to nullable first, since we do not have values here yet - op.add_column("recipes", sa.Column("name_normalized", sa.String(), nullable=True)) + # Set column default first, since we do not have values here yet + op.add_column("recipes", sa.Column("name_normalized", sa.String(), nullable=False, server_default="")) op.add_column("recipes", sa.Column("description_normalized", sa.String(), nullable=True)) op.drop_index("ix_recipes_description", table_name="recipes") op.drop_index("ix_recipes_name", table_name="recipes") @@ -95,9 +110,9 @@ def upgrade(): unique=False, ) do_data_migration() - # Make recipes.name_normalized not nullable now that column should be filled for all rows + # Remove server default now that column should be filled for all rows with op.batch_alter_table("recipes", schema=None) as batch_op: - batch_op.alter_column("name_normalized", nullable=False, existing_type=sa.String()) + batch_op.alter_column("name_normalized", existing_type=sa.String(), server_default=None) # ### end Alembic commands ### diff --git a/alembic/versions/2023-02-21-22.03.19_b04a08da2108_added_shopping_list_label_settings.py b/alembic/versions/2023-02-21-22.03.19_b04a08da2108_added_shopping_list_label_settings.py index da426cb7a04b..edca93c2b6a4 100644 --- a/alembic/versions/2023-02-21-22.03.19_b04a08da2108_added_shopping_list_label_settings.py +++ b/alembic/versions/2023-02-21-22.03.19_b04a08da2108_added_shopping_list_label_settings.py @@ -24,10 +24,10 @@ depends_on = None def populate_shopping_lists_multi_purpose_labels(shopping_lists_multi_purpose_labels_table: sa.Table, session: Session): shopping_lists = session.query(ShoppingList).all() - labels = session.query(MultiPurposeLabel).all() shopping_lists_labels_data: list[dict] = [] for shopping_list in shopping_lists: + labels = session.query(MultiPurposeLabel).filter(MultiPurposeLabel.group_id == ShoppingList.group_id).all() for i, label in enumerate(labels): shopping_lists_labels_data.append( {"id": uuid4(), "shopping_list_id": shopping_list.id, "label_id": label.id, "position": i} diff --git a/alembic/versions/2023-08-06-21.00.34_04ac51cbe9a4_added_group_slug.py b/alembic/versions/2023-08-06-21.00.34_04ac51cbe9a4_added_group_slug.py index 9d846adf4dbc..be341fde57f4 100644 --- a/alembic/versions/2023-08-06-21.00.34_04ac51cbe9a4_added_group_slug.py +++ b/alembic/versions/2023-08-06-21.00.34_04ac51cbe9a4_added_group_slug.py @@ -24,17 +24,22 @@ def populate_group_slugs(session: Session): seen_slugs: set[str] = set() for group in groups: original_name = group.name + new_name = original_name attempts = 0 while True: - slug = slugify(group.name) + slug = slugify(new_name) if slug not in seen_slugs: break attempts += 1 - group.name = f"{original_name} ({attempts})" + new_name = f"{original_name} ({attempts})" seen_slugs.add(slug) - group.slug = slug + session.execute( + sa.text(f"UPDATE {Group.__tablename__} SET name=:name, slug=:slug WHERE id=:id").bindparams( + name=new_name, slug=slug, id=group.id + ) + ) session.commit() diff --git a/alembic/versions/2023-10-04-14.29.26_dded3119c1fe_added_unique_constraints.py b/alembic/versions/2023-10-04-14.29.26_dded3119c1fe_added_unique_constraints.py index 885e619f8199..67c648028d75 100644 --- a/alembic/versions/2023-10-04-14.29.26_dded3119c1fe_added_unique_constraints.py +++ b/alembic/versions/2023-10-04-14.29.26_dded3119c1fe_added_unique_constraints.py @@ -69,9 +69,11 @@ def _resolve_duplicate_food( ): recipe_ingredient.food_id = keep_food_id + session.commit() session.execute( sa.text(f"DELETE FROM {IngredientFoodModel.__tablename__} WHERE id=:id").bindparams(id=dupe_food_id) ) + session.commit() def _resolve_duplicate_unit( @@ -85,9 +87,11 @@ def _resolve_duplicate_unit( for recipe_ingredient in session.query(RecipeIngredientModel).filter_by(unit_id=dupe_unit_id).all(): recipe_ingredient.unit_id = keep_unit_id + session.commit() session.execute( sa.text(f"DELETE FROM {IngredientUnitModel.__tablename__} WHERE id=:id").bindparams(id=dupe_unit_id) ) + session.commit() def _resolve_duplicate_label( @@ -101,7 +105,9 @@ def _resolve_duplicate_label( for ingredient_food in session.query(IngredientFoodModel).filter_by(label_id=dupe_label_id).all(): ingredient_food.label_id = keep_label_id + session.commit() session.execute(sa.text(f"DELETE FROM {MultiPurposeLabel.__tablename__} WHERE id=:id").bindparams(id=dupe_label_id)) + session.commit() def _resolve_duplicate_foods_units_labels(session: Session): @@ -140,6 +146,7 @@ def _remove_duplicates_from_m2m_table(session: Session, table_meta: TableMeta): ) session.execute(query) + session.commit() def _remove_duplicates_from_m2m_tables(session: Session, table_metas: list[TableMeta]): diff --git a/dev/code-generation/utils/anonymize_backups.py b/dev/code-generation/utils/anonymize_backups.py new file mode 100644 index 000000000000..58ee02b7d06b --- /dev/null +++ b/dev/code-generation/utils/anonymize_backups.py @@ -0,0 +1,74 @@ +import json +import logging +import random +import string +from datetime import datetime +from uuid import UUID + +logger = logging.getLogger("anonymize_backups") + + +def is_uuid4(value: str): + try: + UUID(value) + return True + except ValueError: + return False + + +def is_iso_datetime(value: str): + try: + datetime.fromisoformat(value) + return True + except ValueError: + return False + + +def random_string(length=10): + return "".join(random.choice(string.ascii_lowercase) for _ in range(length)) + + +def clean_value(value): + try: + match value: + # preserve non-strings + case int(value) | float(value): + return value + case None: + return value + # preserve UUIDs and datetimes + case str(value) if is_uuid4(value) or is_iso_datetime(value): + return value + # randomize strings + case str(value): + return random_string() + case _: + pass + + except Exception as e: + logger.exception(e) + + logger.error(f"Failed to anonymize value: {value}") + return value + + +def walk_data_and_anonymize(data): + for k, v in data.items(): + if isinstance(v, list): + for item in v: + walk_data_and_anonymize(item) + else: + # preserve alembic version number and enums + if k in ["auth_method", "version_num"]: + continue + + data[k] = clean_value(v) + + +def anonymize_database_json(input_filepath: str, output_filepath: str): + with open(input_filepath) as f: + data = json.load(f) + + walk_data_and_anonymize(data) + with open(output_filepath, "w") as f: + json.dump(data, f) diff --git a/docs/docs/assets/img/pre-v1-backup-location.png b/docs/docs/assets/img/pre-v1-backup-location.png new file mode 100644 index 000000000000..9c68fcf342f6 Binary files /dev/null and b/docs/docs/assets/img/pre-v1-backup-location.png differ diff --git a/docs/docs/documentation/community-guide/home-assistant.md b/docs/docs/documentation/community-guide/home-assistant.md index 6d1793ad72b1..f4ab3dae5966 100644 --- a/docs/docs/documentation/community-guide/home-assistant.md +++ b/docs/docs/documentation/community-guide/home-assistant.md @@ -1,5 +1,5 @@ !!! info -This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed! + This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed! In a lot of ways, Home Assistant is why this project exists! Since Mealie has a robust API it makes it a great fit for interacting with Home Assistant and pulling information into your dashboard. diff --git a/docs/docs/documentation/getting-started/faq.md b/docs/docs/documentation/getting-started/faq.md index cdc1227e8fab..76bb5693837a 100644 --- a/docs/docs/documentation/getting-started/faq.md +++ b/docs/docs/documentation/getting-started/faq.md @@ -41,22 +41,15 @@ Yes. If you are using the v1 branches (including beta), you can upgrade to the l ## How can I change the theme? -You can change the theme by settings the environment variables on the frontend container. +You can change the theme by settings the environment variables. -- [Frontend Theme](../installation/frontend-config#themeing) - -## How can I change the language? - -Languages need to be set on the frontend and backend containers as ENV variables. - -- [Frontend Config](../installation/frontend-config/) -- [Backend Config](../installation/backend-config/) +- [Backend Config - Themeing](./installation/backend-config.md#themeing) ## How can I change the Login Session Timeout? Login session can be configured by setting the `TOKEN_TIME` variable on the backend container. -- [Backend Config](../installation/backend-config/) +- [Backend Config](./installation/backend-config.md) ## Can I serve Mealie on a subpath? @@ -105,8 +98,9 @@ python /app/mealie/scripts/change_password.py Managing private groups and recipes can be confusing. The following diagram and notes should help explain how they work to determine if a recipe can be shared publicly. -- Private links that are generated using the `Share` button bypass all group and recipe permissions. +- Private links that are generated from the recipe page using the `Share` button bypass all group and recipe permissions - Private groups block all access to recipes, including those that are public, except as noted above. +- Groups with "Allow users outside of your group to see your recipes" disabled block all access to recipes, except as noted above. - Private recipes block all access to the recipe from public links. This does not affect Private Links. ```mermaid @@ -130,6 +124,8 @@ stateDiagram-v2 p3 --> n1: No ``` +For more information, check out the [Permissions and Public Access guide](./usage/permissions-and-public-access.md). + ## Can I use fail2ban with mealie? Yes, mealie is configured to properly forward external IP addresses into the `mealie.log` logfile. Note that due to restrictions in docker, IP address forwarding only works on Linux. diff --git a/docs/docs/documentation/getting-started/migrating-to-mealie-v1.md b/docs/docs/documentation/getting-started/migrating-to-mealie-v1.md index ff7dfad23210..a6d3d938edaf 100644 --- a/docs/docs/documentation/getting-started/migrating-to-mealie-v1.md +++ b/docs/docs/documentation/getting-started/migrating-to-mealie-v1.md @@ -16,19 +16,15 @@ The version 1 release of Mealie should be seen as an entirely different applicat ## Migration Considerations -Before you migrate to v1.0.0-beta-x please consider the following: +Before you migrate to v1.0.0 please consider the following: **API Integration Will Break** Several of the endpoints in the API have changed. This means that you will need to update your code to use the new endpoints. -**Meal Plan Notifications Are Not Yet Implemented** +**Recipes Are Private By Default** -If you're using the Meal Plan webhook feature it has yet to be implemented in v1. This feature is being significantly improved in v1 and has yet to be fully fleshed out. If you were a heavy user, you may want to wait until v1 to use this feature. - -**Recipes are Now Private** - -This can be a plus or a minus depending on your use case. If you relied on the old implementation that allowed viewing of recipes without logging in, you will loose that access. We are planning on implementing a public facing interface for groups/tenants to allow unauthenticated users to view public recipes. +By default, recipes can only be viewed by logged-in users. You can fine-tune public recipe access, or keep your instance fully private. For more information, check out the [Permissions and Public Access guide](../getting-started/usage/permissions-and-public-access.md). ## Step 1: Setting Up The New Application @@ -37,7 +33,9 @@ Given the nature of the upgrade, it is highly recommended that you stand up a ne ## Step 2: Exporting Your Data from Pre-v1 -In your instance of Mealie prior to v1, perform an export of your data in the Admin section. Be sure to include the recipes when performing the export. Checking additional items won't impact the migration, but they will be ignored if they are included. +In your instance of Mealie prior to v1, perform an export (backup) of your data in the Admin section. Be sure to include the recipes when performing the export. Checking additional items won't impact the migration, but they will be ignored if they are included. The backups section is located on the admin dashboard in the section labeled "Backups": + +![pre-v1-backup-location-image](../../assets/img/pre-v1-backup-location.png) ## Step 3: Using the Migration Tool diff --git a/docs/docs/documentation/getting-started/usage/permissions-and-public-access.md b/docs/docs/documentation/getting-started/usage/permissions-and-public-access.md new file mode 100644 index 000000000000..f7a3c1043f26 --- /dev/null +++ b/docs/docs/documentation/getting-started/usage/permissions-and-public-access.md @@ -0,0 +1,57 @@ +# Permissions and Public Access + +Mealie provides various levels of user access and permissions. This includes: +- Authentication and registration ([check out the LDAP guide](./ldap.md) for how to configure access using LDAP) +- Customizable user permissions +- Fine-tuned public access for non-users + +## Customizable User Permissions + +Each user can be configured to have varying levels of access. Some of these permissions include: +- Access to Administrator tools +- Access to inviting other users +- Access to manage their group and group data + +Administrators can navigate to the Settings page and access the User Management page to configure these settings. + + +[User Management Demo](https://demo.mealie.io/admin/manage/users){ .md-button .md-button--primary } + +## Public Recipe Access + +By default, groups are set to private, meaning only logged-in users may access the group. In order for a recipe to be viewable by public (not logged-in) users, two criteria must be met: + +1. The group must not be private, *and* the group setting for allowing users outside of your group to see your recipes must be enabled. These can be toggled on the Group Settings page +2. The recipe must be set to public. This can be toggled for each recipe individually, or in bulk using the Recipe Data Management page + +Additionally, if the group is not private, public users can view all public group data (public recipes, public cookbooks, etc.) from the home page ([e.g. the demo home page](https://demo.mealie.io/g/home)). + +[Group Settings Demo](https://demo.mealie.io/group){ .md-button .md-button--primary } + +More broadly, here are the rules for how recipe access is determined: + +- Private links that are generated from the recipe page using the `Share` button bypass all group and recipe permissions +- Private groups block all access to recipes, including those that are public, except as noted above. +- Groups with "Allow users outside of your group to see your recipes" disabled block all access to recipes, except as noted above. +- Private recipes block all access to the recipe from public links. This does not affect Private Links. + +```mermaid +stateDiagram-v2 + r1: Request Access + p1: Using Private Link? + p2: Is Group Private? + p3: Is Recipe Private? + s1: Deny Access + n1: Allow Access + + + r1 --> p1 + p1 --> p2: No + p1 --> n1: Yes + + p2 --> s1: Yes + p2 --> p3: No + + p3 --> s1: Yes + p3 --> n1: No +``` diff --git a/docs/docs/overrides/api.html b/docs/docs/overrides/api.html index 83038e2aead5..2d2576ae0aaf 100644 --- a/docs/docs/overrides/api.html +++ b/docs/docs/overrides/api.html @@ -14,7 +14,7 @@
diff --git a/frontend/components/Domain/Recipe/RecipeActionMenu.vue b/frontend/components/Domain/Recipe/RecipeActionMenu.vue index ad24e0ed8b13..4fd418ffbb34 100644 --- a/frontend/components/Domain/Recipe/RecipeActionMenu.vue +++ b/frontend/components/Domain/Recipe/RecipeActionMenu.vue @@ -139,7 +139,7 @@ export default defineComponent({ default: false, }, }, - setup(props, context) { + setup(_, context) { const deleteDialog = ref(false); const { i18n, $globals } = useContext(); diff --git a/frontend/components/Domain/Recipe/RecipeNotes.vue b/frontend/components/Domain/Recipe/RecipeNotes.vue index f5edcaaf672a..2139ef0a9ad4 100644 --- a/frontend/components/Domain/Recipe/RecipeNotes.vue +++ b/frontend/components/Domain/Recipe/RecipeNotes.vue @@ -1,7 +1,7 @@