Fix fresh-install schema gap and OIDC provider CA trust

- Session::prepareTablesIfNeeded(): Wt::Dbo's automatic createTables()
  silently fails to create 4 tables (share, starred_artist,
  starred_release, starred_track) on a genuinely fresh database, with
  no exception raised and no clear reason found in Wt::Dbo internals.
  These tables are otherwise only ever created via historical schema
  migrations, which never run for a fresh install (new DBs are stamped
  directly at LMS_DATABASE_VERSION). Reproduced consistently across
  multiple from-scratch database attempts. Added defensive
  CREATE TABLE IF NOT EXISTS statements for all 4 right after
  createTables() as a targeted workaround; harmless no-op on databases
  where they already exist correctly.

- Dockerfile-release / docker-entrypoint.sh: the OIDC token exchange
  (OAuthProcess::handleToken(), a server-to-server HTTPS call from LMS
  to the provider's token endpoint) has no way to trust a private/
  internal CA, so any OIDC provider behind non-publicly-trusted TLS
  fails with 'certificate verify failed' immediately after the user
  authenticates - the callback silently drops back to the login page
  with no user-visible error, and no account ever gets created (the
  auto-registration logic in OIDCAuth::OIDCAuth was already correct;
  it just never got a chance to run). Added ca-certificates + su-exec
  to the runtime image and a docker-entrypoint.sh that runs
  update-ca-certificates against anything mounted into
  /usr/local/share/ca-certificates before dropping privileges and
  exec'ing lms, so operators can trust a custom CA via a volume mount.
This commit is contained in:
2026-08-20 01:00:37 -05:00
parent beb190c518
commit 6d0f8fcc5a
3 changed files with 82 additions and 3 deletions
+9 -3
View File
@@ -155,6 +155,7 @@ ARG RUNTIME_PACKAGES=" \
boost-iostreams \
boost-program_options \
boost-thread \
ca-certificates \
lame-libs \
libarchive \
libconfig++ \
@@ -170,6 +171,7 @@ ARG RUNTIME_PACKAGES=" \
opus \
pugixml \
sqlite-libs \
su-exec \
zlib"
ARG LMS_USER=lms
@@ -191,12 +193,16 @@ RUN addgroup -S ${LMS_GROUP} && \
VOLUME /var/lms
VOLUME /music
VOLUME /usr/local/etc
USER ${LMS_USER}:${LMS_GROUP}
VOLUME /usr/local/share/ca-certificates
COPY --from=build /tmp/fakeroot/ /usr
COPY --from=build /tmp/fakeroot/share/lms/lms.conf /etc/lms.conf
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
EXPOSE 5082
ENTRYPOINT ["/usr/bin/lms"]
# Stays root so the entrypoint can run update-ca-certificates; it drops to
# ${LMS_USER}:${LMS_GROUP} via su-exec before running lms itself.
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["/usr/bin/lms"]
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
# If the operator mounted extra trusted CA certificates (e.g. an internal
# PKI root, common when LMS's OIDC provider sits behind a private CA),
# pick them up before starting. update-ca-certificates needs root, so this
# runs before we drop privileges to the lms user.
set -e
if [ -d /usr/local/share/ca-certificates ] && [ -n "$(ls -A /usr/local/share/ca-certificates 2>/dev/null)" ]; then
update-ca-certificates
fi
exec su-exec lms:lms "$@"
+61
View File
@@ -182,6 +182,67 @@ namespace lms::db
throw e;
}
}
// Workaround: on a genuinely fresh database, Wt::Dbo's createTables()
// above silently fails to create "share", "starred_artist",
// "starred_release" and "starred_track" (no exception raised, and no
// discernible difference from the ~57 other mapped classes that DO
// get created correctly). Root cause not identified. These four are
// otherwise only ever created via historical schema migrations
// (Migration.cpp), which never run for a fresh install (a new
// database is stamped directly at LMS_DATABASE_VERSION). Create them
// here defensively; IF NOT EXISTS makes this a no-op on databases
// where createTables() (or, on upgraded databases, the migration
// path) already created them correctly.
{
auto transaction{ createWriteTransaction() };
execute(R"(CREATE TABLE IF NOT EXISTS "share" (
"id" integer primary key autoincrement,
"version" integer not null,
"token" text not null,
"media_ids" text not null,
"description" text not null,
"created" text not null,
"expires" text,
"last_visited" text,
"visit_count" integer not null,
"user_id" bigint not null,
constraint "fk_share_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
))");
execute(R"(CREATE TABLE IF NOT EXISTS "starred_artist" (
"id" integer primary key autoincrement,
"version" integer not null,
"backend" integer not null,
"sync_state" integer not null,
"date_time" text,
"artist_id" bigint,
"user_id" bigint,
constraint "fk_starred_artist_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred,
constraint "fk_starred_artist_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
))");
execute(R"(CREATE TABLE IF NOT EXISTS "starred_release" (
"id" integer primary key autoincrement,
"version" integer not null,
"backend" integer not null,
"sync_state" integer not null,
"date_time" text,
"release_id" bigint,
"user_id" bigint,
constraint "fk_starred_release_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred,
constraint "fk_starred_release_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
))");
execute(R"(CREATE TABLE IF NOT EXISTS "starred_track" (
"id" integer primary key autoincrement,
"version" integer not null,
"backend" integer not null,
"sync_state" integer not null,
"date_time" text,
"track_id" bigint,
"user_id" bigint,
constraint "fk_starred_track_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
constraint "fk_starred_track_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
))");
}
}
bool Session::migrateSchemaIfNeeded()