Compare commits

..
Author SHA1 Message Date
ajnart 4ab9f25274 fix(integrations): gate mock integration server-side 2026-09-06 09:47:35 +02:00
8 changed files with 67 additions and 40 deletions

No files matched your search

+9 -11
View File
@@ -1,24 +1,22 @@
# Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo. # Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo.
# Keep this file up-to-date when you add new variables to `.env`. # Keep this file up-to-date when you add new variables to \`.env\`.
# This file will be committed to version control, so make sure not to have any secrets in it. # This file will be committed to version control, so make sure not to have any secrets in it.
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets. # If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
# Generate two different 64-character hex values with `openssl rand -hex 32` before starting Homarr. # The below secret is not used anywhere but required for Auth.js (Would encrypt JWTs and Mail hashes, both not used)
# Never reuse these values or commit the populated `.env` file. AUTH_SECRET="supersecret"
AUTH_SECRET=
# The below secret is used to encrypt integration secrets in the database. # The below secret is used to encrypt integration secrets in the database.
# Homarr will reject an empty or invalid value and print a randomly generated candidate. # It should be a 32-byte string, generated by running `openssl rand -hex 32` on Unix
SECRET_ENCRYPTION_KEY= # or starting the project without any (which will show a randomly generated one).
SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
LOG_LEVEL='info' LOG_LEVEL='info'
# This is how you can use the sqlite driver: # This is how you can use the sqlite driver:
DB_DRIVER='better-sqlite3' DB_DRIVER='better-sqlite3'
# Safe local source-development path. SQLite files are ignored by Git. DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
# Container images use /appdata/db/db.sqlite by default instead.
DB_URL='./db.sqlite'
# Those are the two ways to use the mysql2 driver: # Those are the two ways to use the mysql2 driver:
# 1. Using the URL format: # 1. Using the URL format:
@@ -60,8 +58,8 @@ TURBO_TELEMETRY_DISABLED=1
# Enable kubernetes tool # Enable kubernetes tool
# ENABLE_KUBERNETES=true # ENABLE_KUBERNETES=true
# Enable mock integration # Enable the development-only mock integration. Never enable this in production.
UNSAFE_ENABLE_MOCK_INTEGRATION=true # UNSAFE_ENABLE_MOCK_INTEGRATION=true
# Public URL contract. Values must be absolute HTTP(S) URLs without credentials, # Public URL contract. Values must be absolute HTTP(S) URLs without credentials,
# query strings, or fragments. Trailing slashes are normalized at startup. # query strings, or fragments. Trailing slashes are normalized at startup.
@@ -133,9 +133,10 @@ External redis is currently not supported with Proxmox Community Scripts
The advanced deployments environment variables should only be used if you know what you are doing. The advanced deployments environment variables should only be used if you know what you are doing.
::: :::
| Environment Variable | Description | | Environment Variable | Description | Default |
| ------------------------ | --------------------------------------------------- | | -------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------- |
| `DB_MIGRATIONS_DISABLED` | Disable db migrations. For example for helm charts. | | `DB_MIGRATIONS_DISABLED` | Disable database migrations. For example, when migrations are managed separately in Helm deployments. | `false` |
| `UNSAFE_ENABLE_MOCK_INTEGRATION` | Enable the development-only Mock integration. This exposes fabricated data and must not be used in production. | `false` |
All public URL variables reject non-HTTP protocols, embedded credentials, query strings, and fragments, and normalize All public URL variables reject non-HTTP protocols, embedded credentials, query strings, and fragments, and normalize
trailing slashes when Homarr or Docusaurus starts. trailing slashes when Homarr or Docusaurus starts.
@@ -244,21 +244,18 @@ Homarr supports multiple authentication options, from internal userbase (credent
- Create a group (e.g., `homarr-admins`) and assign users who require admin access. - Create a group (e.g., `homarr-admins`) and assign users who require admin access.
**2. Example .env file:** **2. Example .env file:**
Generate `SECRET_ENCRYPTION_KEY` with `openssl rand -hex 32`; do not copy an example key between installs.
```env ```env
OIDC_CLIENT_ID=<REPLACE_WITH_OIDC_CLIENT_ID> OIDC_CLIENT_ID=identificationid #OIDC client ID
OIDC_CLIENT_SECRET=<REPLACE_WITH_OIDC_CLIENT_SECRET> OIDC_CLIENT_SECRET=secretsecretsecret #OIDC client secret
OIDC_SLUG=homarr #Application slug in Authentik OIDC_SLUG=homarr #Application slug in Authentik
AUTH_DOMAIN=auth.example.com #Authentik FQDN AUTH_DOMAIN=auth.example.com #Authentik FQDN
ADMIN_GROUP=homarr-admins #Authentik group for Homarr admins ADMIN_GROUP=homarr-admins #Authentik group for Homarr admins
HOMARR_FQDN=homarr.example.com #Homarr FQDN HOMARR_FQDN=homarr.example.com #Homarr FQDN
SECRET_ENCRYPTION_KEY=<REPLACE_WITH_64_HEX_CHARACTERS> SECRET_ENCRYPTION_KEY=encryptencryptencrypt #Homarr encryption key
``` ```
**3. Example Docker Compose configuration:** **3. Example Docker Compose configuration:**
```yaml {14-24} ```yaml {14-26}
services: services:
homarr: homarr:
image: ghcr.io/homarr-labs/homarr:latest image: ghcr.io/homarr-labs/homarr:latest
@@ -272,6 +269,8 @@ Homarr supports multiple authentication options, from internal userbase (credent
environment: environment:
- TZ=America/Los_Angeles - TZ=America/Los_Angeles
- SECRET_ENCRYPTION_KEY=${SECRET_ENCRYPTION_KEY} - SECRET_ENCRYPTION_KEY=${SECRET_ENCRYPTION_KEY}
- BASE_URL=https://${HOMARR_FQDN}
- NEXTAUTH_URL=https://${HOMARR_FQDN}
- AUTH_PROVIDERS=oidc #(optional: include ',credentials' to keep local accounts as fallback) - AUTH_PROVIDERS=oidc #(optional: include ',credentials' to keep local accounts as fallback)
- AUTH_OIDC_CLIENT_ID=${OIDC_CLIENT_ID} - AUTH_OIDC_CLIENT_ID=${OIDC_CLIENT_ID}
- AUTH_OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET} - AUTH_OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}
@@ -323,18 +322,15 @@ Homarr supports multiple authentication options, from internal userbase (credent
- Record the `Client-ID` and `Client Key` (secret). - Record the `Client-ID` and `Client Key` (secret).
**2. Example .env file (Homarr):** **2. Example .env file (Homarr):**
Generate `SECRET_ENCRYPTION_KEY` with `openssl rand -hex 32`; do not copy an example key between installs.
```env ```env
OIDC_CLIENT_ID=<REPLACE_WITH_GOOGLE_CLIENT_ID> OIDC_CLIENT_ID=identificationid #Google Client-ID
OIDC_CLIENT_SECRET=<REPLACE_WITH_GOOGLE_CLIENT_SECRET> OIDC_CLIENT_SECRET=secretsecretsecret #Google Client Key (secret)
HOMARR_FQDN=homarr.example.com #Homarr FQDN HOMARR_FQDN=homarr.example.com #Homarr FQDN
SECRET_ENCRYPTION_KEY=<REPLACE_WITH_64_HEX_CHARACTERS> SECRET_ENCRYPTION_KEY=encryptencryptencrypt #Homarr encryption key
``` ```
**3. Example Docker Compose configuration (Homarr):** **3. Example Docker Compose configuration (Homarr):**
```yaml {14-20} ```yaml {16-22}
services: services:
homarr: homarr:
image: ghcr.io/homarr-labs/homarr:latest image: ghcr.io/homarr-labs/homarr:latest
@@ -348,6 +344,8 @@ Homarr supports multiple authentication options, from internal userbase (credent
environment: environment:
- TZ=Europe/Berlin - TZ=Europe/Berlin
- SECRET_ENCRYPTION_KEY=${SECRET_ENCRYPTION_KEY} - SECRET_ENCRYPTION_KEY=${SECRET_ENCRYPTION_KEY}
- BASE_URL=https://${HOMARR_FQDN}
- NEXTAUTH_URL=https://${HOMARR_FQDN}
- AUTH_PROVIDERS=oidc #(optional: include ',credentials' to keep local accounts as fallback) - AUTH_PROVIDERS=oidc #(optional: include ',credentials' to keep local accounts as fallback)
- AUTH_OIDC_CLIENT_ID=${OIDC_CLIENT_ID} - AUTH_OIDC_CLIENT_ID=${OIDC_CLIENT_ID}
- AUTH_OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET} - AUTH_OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}
@@ -38,14 +38,13 @@ This method is only useful, when you want to develop on Homarr or extend it with
Install all dependencies using <code>pnpm install</code> Install all dependencies using <code>pnpm install</code>
</li> </li>
<li> <li>
Copy the `.env.example` file to `.env`. Its `DB_URL='./db.sqlite'` value is ready for local use and stores the Copy the `.env.example` file to `.env` and replace `DB_URL` with the full path you want to use for the sqlite
database in the cloned repository. Replace it with an absolute writable path such as database file. For example `DB_URL='/home/username/homarr/db.sqlite`
`DB_URL='/home/username/homarr/db.sqlite'` if you want to store the database elsewhere.
</li> </li>
<li> <li>
Run `openssl rand -hex 32` twice and set `AUTH_SECRET` and `SECRET_ENCRYPTION_KEY` to the two different generated Replace `AUTH_SECRET` and `SECRET_ENCRYPTION_KEY` both with a secure random string by using `openssl rand -hex 32`
values. Keep both values secret and preserve `SECRET_ENCRYPTION_KEY` with your backups.
</li> </li>
<li>Replace `CRON_JOB_API_KEY` with a secure random string by using `openssl rand -base64 32`</li>
<li>Run `pnpm run db:migration:sqlite:run` and wait that it completes</li> <li>Run `pnpm run db:migration:sqlite:run` and wait that it completes</li>
<li>Build the source using `pnpm build`</li> <li>Build the source using `pnpm build`</li>
<li> <li>
@@ -47,6 +47,9 @@ export default async function IntegrationsNewPage(props: NewIntegrationPageProps
const tCreate = await getI18n("integration.page.create"); const tCreate = await getI18n("integration.page.create");
const currentKind = result.data; const currentKind = result.data;
if (currentKind === "mock" && !env.UNSAFE_ENABLE_MOCK_INTEGRATION) {
notFound();
}
return ( return (
<> <>
+2
View File
@@ -19,6 +19,7 @@ const publicHttpUrl = (variableName: string) =>
export const env = createEnv({ export const env = createEnv({
server: { server: {
KUBERNETES_SERVICE_ACCOUNT_NAME: z.string().optional(), KUBERNETES_SERVICE_ACCOUNT_NAME: z.string().optional(),
UNSAFE_ENABLE_MOCK_INTEGRATION: createBooleanSchema(false),
DEMO_MODE: createBooleanSchema(false), DEMO_MODE: createBooleanSchema(false),
DEMO_READ_ONLY: createBooleanSchema(true), DEMO_READ_ONLY: createBooleanSchema(true),
HOMARR_WEBSITE_URL: publicHttpUrl("HOMARR_WEBSITE_URL").default("https://homarr.dev"), HOMARR_WEBSITE_URL: publicHttpUrl("HOMARR_WEBSITE_URL").default("https://homarr.dev"),
@@ -27,6 +28,7 @@ export const env = createEnv({
}, },
runtimeEnv: { runtimeEnv: {
KUBERNETES_SERVICE_ACCOUNT_NAME: process.env.KUBERNETES_SERVICE_ACCOUNT_NAME, KUBERNETES_SERVICE_ACCOUNT_NAME: process.env.KUBERNETES_SERVICE_ACCOUNT_NAME,
UNSAFE_ENABLE_MOCK_INTEGRATION: process.env.UNSAFE_ENABLE_MOCK_INTEGRATION,
DEMO_MODE: process.env.DEMO_MODE, DEMO_MODE: process.env.DEMO_MODE,
DEMO_READ_ONLY: process.env.DEMO_READ_ONLY, DEMO_READ_ONLY: process.env.DEMO_READ_ONLY,
HOMARR_WEBSITE_URL: process.env.HOMARR_WEBSITE_URL, HOMARR_WEBSITE_URL: process.env.HOMARR_WEBSITE_URL,
@@ -18,7 +18,12 @@ import {
integrationSecrets, integrationSecrets,
integrationUserPermissions, integrationUserPermissions,
} from "@homarr/db/schema"; } from "@homarr/db/schema";
import type { GroupPermissionKey, IntegrationPermission, IntegrationSecretKind } from "@homarr/definitions"; import type {
GroupPermissionKey,
IntegrationKind,
IntegrationPermission,
IntegrationSecretKind,
} from "@homarr/definitions";
import { import {
getIntegrationKindsByCategory, getIntegrationKindsByCategory,
getPermissionsWithParents, getPermissionsWithParents,
@@ -39,6 +44,7 @@ import {
} from "@homarr/validation/integration"; } from "@homarr/validation/integration";
import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request"; import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request";
import { env } from "../../env";
import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
import { throwIfActionForbiddenAsync } from "./integration-access"; import { throwIfActionForbiddenAsync } from "./integration-access";
@@ -48,6 +54,15 @@ import { mapTestConnectionError } from "./map-test-connection-error";
const logger = createLogger({ module: "integrationRouter" }); const logger = createLogger({ module: "integrationRouter" });
const mediaRequestSearchKinds = getIntegrationKindsByCategory("mediaSearch"); const mediaRequestSearchKinds = getIntegrationKindsByCategory("mediaSearch");
const throwIfMockIntegrationIsDisabled = (kind: IntegrationKind) => {
if (kind !== "mock" || env.UNSAFE_ENABLE_MOCK_INTEGRATION) return;
throw new TRPCError({
code: "FORBIDDEN",
message: "Mock integrations are disabled",
});
};
export const integrationRouter = createTRPCRouter({ export const integrationRouter = createTRPCRouter({
getKinds: publicProcedure getKinds: publicProcedure
.meta({ .meta({
@@ -58,12 +73,14 @@ export const integrationRouter = createTRPCRouter({
}, },
}) })
.query(() => { .query(() => {
return objectEntries(integrationDefs).map(([kind, def]) => ({ return objectEntries(integrationDefs)
kind, .filter(([kind]) => kind !== "mock" || env.UNSAFE_ENABLE_MOCK_INTEGRATION)
name: def.name, .map(([kind, def]) => ({
category: def.category, kind,
requiredSecrets: def.secretKinds, name: def.name,
})); category: def.category,
requiredSecrets: def.secretKinds,
}));
}), }),
all: protectedProcedure all: protectedProcedure
.meta({ .meta({
@@ -229,6 +246,8 @@ export const integrationRouter = createTRPCRouter({
}) })
.input(integrationCreateSchema) .input(integrationCreateSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
throwIfMockIntegrationIsDisabled(input.kind);
logger.info("Creating integration", { logger.info("Creating integration", {
name: input.name, name: input.name,
kind: input.kind, kind: input.kind,
@@ -321,6 +321,13 @@ export const onboardRouter = createTRPCRouter({
.requiresStep("setup") .requiresStep("setup")
.input(onboardingCompleteSetupSchema) .input(onboardingCompleteSetupSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
if (
!env.UNSAFE_ENABLE_MOCK_INTEGRATION &&
input.integrations.some((integration) => integration.kind === "mock")
) {
throw new TRPCError({ code: "FORBIDDEN", message: "Mock integrations are disabled" });
}
const boardList = await ctx.db.query.boards.findMany({ const boardList = await ctx.db.query.boards.findMany({
with: { with: {
sections: { with: { layouts: true } }, sections: { with: { layouts: true } },