Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,6 @@ When connecting to an SQLite database, the project files are available under the
```bash
ddev dotenv set .ddev/.env.adminer \
--adminer-default-driver=sqlite \
--adminer-default-username="" \
--adminer-default-password="" \
--adminer-default-server="" \
--adminer-default-db=/mnt/ddev_app/test.sqlite
```

Expand All @@ -62,7 +59,7 @@ To add more plugins:

```bash
# plugins: https://www.adminer.org/en/plugins/
ddev dotenv set .ddev/.env.adminer --adminer-plugins="ddev-passwordless-login tables-filter edit-calendar"
ddev dotenv set .ddev/.env.adminer --adminer-plugins="tables-filter edit-calendar"
ddev add-on get ddev/ddev-adminer
ddev restart
```
Expand All @@ -82,8 +79,8 @@ All customization options (use with caution):
| `ADMINER_DEFAULT_PASSWORD` | `--adminer-default-password` | `db` |
| `ADMINER_DEFAULT_USERNAME` | `--adminer-default-username` | `db` |
| `ADMINER_DESIGN` | `--adminer-design` | `` |
| `ADMINER_DOCKER_IMAGE` | `--adminer-docker-image` | `adminer:standalone` |
| `ADMINER_PLUGINS` | `--adminer-plugins` | `ddev-passwordless-login tables-filter` |
| `ADMINER_DOCKER_IMAGE` | `--adminer-docker-image` | `adminer:6-standalone` |
| `ADMINER_PLUGINS` | `--adminer-plugins` | `tables-filter` |

## Credits

Expand Down
112 changes: 39 additions & 73 deletions docker-compose.adminer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,67 +2,33 @@
services:
adminer:
container_name: ddev-${DDEV_SITENAME}-adminer
image: ${ADMINER_DOCKER_IMAGE:-adminer:standalone}-${DDEV_SITENAME}-built
build:
dockerfile_inline: |
ARG ADMINER_DOCKER_IMAGE="scratch"
FROM $$ADMINER_DOCKER_IMAGE

# Switch the adminer user.
USER root
# Accept build arguments for user creation
ARG username
ARG uid
ARG gid
# Free up the uid and gid if they're currently used, but ensure the original adminer group still exists
RUN existing_user=$(getent passwd "$$uid" | cut -d: -f1); \
existing_group=$(getent group "$$gid" | cut -d: -f1); \
existing_adminer_gid=$(getent group adminer | cut -d: -f3); \
if [[ -n "$$existing_group" ]]; then echo "Deleting existing group: $$existing_group ($$gid)"; delgroup "$$existing_group"; fi; \
if [[ -n "$$existing_user" ]]; then echo "Deleting existing user: $$existing_user ($$uid)"; deluser "$$existing_user"; fi; \
addgroup -g "$$existing_adminer_gid" adminer || addgroup adminer || true
# Ensure tty group exists
RUN getent group tty || addgroup tty
# Create group and user, trying multiple methods for compatibility
RUN (addgroup -g "$$gid" "$$username" || addgroup "$$username" || true) && \
(adduser -G tty -s "/bin/sh" -D -u "$$uid" "$$username" || \
adduser -G tty -s "/bin/sh" -D "$$username" || \
adduser -G tty -s "/bin/sh" "$$username") && \
addgroup "$$username" adminer
# Ensure the plugins-enabled directory is writable by the group.
RUN chmod 775 /var/www/html/plugins-enabled
# Switch to the created user
USER "$$username"
args:
ADMINER_DOCKER_IMAGE: ${ADMINER_DOCKER_IMAGE:-adminer:standalone}
username: ${DDEV_USER}
uid: ${DDEV_UID}
gid: ${DDEV_GID}
image: ${ADMINER_DOCKER_IMAGE:-adminer:6-standalone}
hostname: ${DDEV_SITENAME}-adminer
# Started as root, entrypoint.sh drops to the host user.
user: root
environment:
- ADMINER_DEFAULT_DRIVER=${ADMINER_DEFAULT_DRIVER:-${DDEV_DATABASE_FAMILY:-server}}
- ADMINER_DEFAULT_SERVER=${ADMINER_DEFAULT_SERVER:-db}
- ADMINER_DEFAULT_DB=${ADMINER_DEFAULT_DB:-db}
- ADMINER_DEFAULT_USERNAME=${ADMINER_DEFAULT_USERNAME:-db}
- ADMINER_DEFAULT_PASSWORD=${ADMINER_DEFAULT_PASSWORD:-db}
- ADMINER_PLUGINS=${ADMINER_PLUGINS:-ddev-passwordless-login tables-filter}
- ADMINER_PLUGINS=${ADMINER_PLUGINS:-tables-filter}
- ADMINER_DESIGN=${ADMINER_DESIGN:-}
- VIRTUAL_HOST=$DDEV_HOSTNAME
- DDEV_UID=${DDEV_UID}
- DDEV_GID=${DDEV_GID}
- VIRTUAL_HOST=${DDEV_HOSTNAME}
- HTTP_EXPOSE=9100:8080
- HTTPS_EXPOSE=9101:8080
labels:
com.ddev.site-name: ${DDEV_SITENAME}
com.ddev.approot: $DDEV_APPROOT
com.ddev.approot: ${DDEV_APPROOT}
volumes:
- ".:/mnt/ddev_config"
- "../:/mnt/ddev_app"
- "ddev-global-cache:/mnt/ddev-global-cache"
depends_on:
- db
command: ["php", "-S", "[::]:8080", "-t", "/var/www/html", "ddev-adminer.php"]
configs:
- source: ddev-adminer.php
target: /var/www/html/ddev-adminer.php
mode: "0444"
- source: ddev-passwordless-login.php
target: /var/www/html/plugins/ddev-passwordless-login.php
mode: "0444"
Expand All @@ -71,43 +37,34 @@ services:
mode: "0755"

configs:
ddev-adminer.php:
content: |
<?php
// Let static files be served directly
$$uri = parse_url($$_SERVER['REQUEST_URI'], PHP_URL_PATH);
if (is_file('/var/www/html' . $$uri)) {
return false;
}

if (!count($$_GET)) {
if ($$_ENV['ADMINER_DEFAULT_DRIVER'] === 'mysql') {
$$_ENV['ADMINER_DEFAULT_DRIVER'] = 'server';
} else if ($$_ENV['ADMINER_DEFAULT_DRIVER'] === 'postgres') {
$$_ENV['ADMINER_DEFAULT_DRIVER'] = 'pgsql';
}
$$_POST['auth'] = [
'driver' => $$_ENV['ADMINER_DEFAULT_DRIVER'],
'server' => $$_ENV['ADMINER_DEFAULT_SERVER'],
'db' => $$_ENV['ADMINER_DEFAULT_DB'],
'username' => $$_ENV['ADMINER_DEFAULT_USERNAME'],
'password' => $$_ENV['ADMINER_DEFAULT_PASSWORD'],
];
}
include './index.php';
?>

ddev-passwordless-login.php:
content: |
<?php
/**
* Adminer plugin for enabling passwordless login in DDEV.
* By default, only the SQLite driver is supported.
* Adminer plugin for logging in with the DDEV credentials.
*
* Adminer connects only with a password from its session, so seed it and
* redirect to the URL holding the other credentials.
*/
class DDEVPasswordlessLogin extends Adminer\Plugin {
function __construct() {
if (count($$_GET)) {
return;
}
$$driver = ['mysql' => 'server', 'postgres' => 'pgsql'][$$_ENV['ADMINER_DEFAULT_DRIVER']] ?? $$_ENV['ADMINER_DEFAULT_DRIVER'];
$$_SESSION['pwds'][$$driver][$$_ENV['ADMINER_DEFAULT_SERVER']][$$_ENV['ADMINER_DEFAULT_USERNAME']] = $$_ENV['ADMINER_DEFAULT_PASSWORD'];
$$_SESSION['db'][$$driver][$$_ENV['ADMINER_DEFAULT_SERVER']][$$_ENV['ADMINER_DEFAULT_USERNAME']][$$_ENV['ADMINER_DEFAULT_DB']] = true;
header('Location: ?' . http_build_query([
$$driver => $$_ENV['ADMINER_DEFAULT_SERVER'],
'username' => $$_ENV['ADMINER_DEFAULT_USERNAME'],
'db' => $$_ENV['ADMINER_DEFAULT_DB'] ?: null,
]));
exit;
}

function login($$login, $$password) {
if (Adminer\JUSH === 'sqlite' && empty($$password)) {
// Adminer refuses a login without a password, SQLite has none.
if (Adminer\JUSH === 'sqlite') {
return true;
}
}
Expand All @@ -117,10 +74,18 @@ configs:
content: |
#!/bin/sh
set -e
# Serve as the host user, so that SQLite databases in the project keep its
# ownership. Only the uid and gid matter, the name is cosmetic.
if [ "$$(id -u)" = 0 ]; then
chown "$$DDEV_UID:$$DDEV_GID" plugins-enabled
grep -q '^ddev:' /etc/passwd || echo "ddev:x:$$DDEV_UID:$$DDEV_GID:::/bin/sh" >> /etc/passwd
exec su ddev -s /bin/sh -c 'exec "$$@"' -- sh "$$0" "$$@"
fi
if [ -n "$$ADMINER_DESIGN" ]; then
# Only create link on initial start, to ensure that explicit changes to
# adminer.css after the container was started once are preserved.
if [ ! -e .adminer-init ]; then
# See https://github.com/TimWolla/docker-adminer/pull/216
for css_file in adminer.css adminer-dark.css; do
if [ -f "designs/$$ADMINER_DESIGN/$$css_file" ]; then
ln -sf "designs/$$ADMINER_DESIGN/$$css_file" adminer.css
Expand All @@ -130,7 +95,8 @@ configs:
fi
fi
number=1
for PLUGIN in $$ADMINER_PLUGINS; do
# Always enable the login, ignoring a duplicate from an older config.
for PLUGIN in $$(printf '%s\n' ddev-passwordless-login $$ADMINER_PLUGINS | awk '!seen[$$0]++'); do
php plugin-loader.php "$$PLUGIN" > plugins-enabled/$$(printf "%03d" $$number)-$$PLUGIN.php
number=$$(($$number+1))
done
Expand Down
51 changes: 45 additions & 6 deletions tests/test.bats
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ setup() {

export ADMINER_DESIGN=""
export TEST_REDIRECT_LOCATION="?server=db&username=db&db=db"
export TEST_PAGE_TITLE="Database: db - db - Adminer"
export TEST_SQLITE_TABLE=""
}

health_checks() {
Expand All @@ -51,6 +53,23 @@ health_checks() {
# Make sure the user is fully authenticated after redirecting
assert_output --partial "HTTP/2 200"

# Make sure the database is shown and not the login form
run curl -sfL --cookie .cookie-jar.txt --cookie-jar .cookie-jar.txt https://${PROJNAME}.ddev.site:9101
assert_success
assert_output --partial "<title>${TEST_PAGE_TITLE}</title>"
refute_output --partial "auth[password]"

# Make sure Adminer serves as the host user, it has to write project files
run ddev exec -s adminer 'stat -c %u /proc/1; echo $DDEV_UID'
assert_success
assert_equal "${lines[0]}" "${lines[1]}"

# Make sure the login is enabled once, next to the plugins from ADMINER_PLUGINS
run ddev exec -s adminer 'ls plugins-enabled'
assert_success
assert_output "001-ddev-passwordless-login.php
002-tables-filter.php"

# Make sure `ddev adminer` works
DDEV_DEBUG=true run ddev adminer
assert_success
Expand All @@ -69,6 +88,25 @@ health_checks() {
assert_success
assert_output --partial "${ADMINER_DESIGN}"
fi

if [ "${TEST_SQLITE_TABLE}" != "" ]; then
# Make sure the table of the database is listed
run curl -sfL --cookie .cookie-jar.txt --cookie-jar .cookie-jar.txt https://${PROJNAME}.ddev.site:9101
assert_success
assert_output --partial "${TEST_SQLITE_TABLE}"

# Make sure Adminer can write the database file owned by the host user,
# it only accepts a write together with its CSRF token
sql_url="https://${PROJNAME}.ddev.site:9101/${TEST_REDIRECT_LOCATION}&sql="
token="$(curl -sf --cookie .cookie-jar.txt --cookie-jar .cookie-jar.txt "${sql_url}" | grep -oE "name='token' value='[0-9]+:[0-9]+" | grep -oE '[0-9]+:[0-9]+')"
run curl -sf --cookie .cookie-jar.txt --cookie-jar .cookie-jar.txt \
--data-urlencode "query=INSERT INTO ${TEST_SQLITE_TABLE}(name) VALUES ('gamma');" \
--data "token=${token}" "${sql_url}"
assert_success
assert_output --partial "Query executed OK"
run ddev exec sqlite3 test.sqlite "SELECT name FROM ${TEST_SQLITE_TABLE} WHERE name = 'gamma';"
assert_output "gamma"
fi
}

teardown() {
Expand Down Expand Up @@ -111,7 +149,9 @@ teardown() {

run ddev config --router-http-port=8080 --router-https-port=8443
assert_success
run ddev dotenv set .ddev/.env.adminer --adminer-design="${ADMINER_DESIGN}"
# ddev-passwordless-login is enabled on its own, older configs may list it
run ddev dotenv set .ddev/.env.adminer --adminer-design="${ADMINER_DESIGN}" \
--adminer-plugins="ddev-passwordless-login tables-filter"
assert_success
assert_file_exist .ddev/.env.adminer
echo "# ddev add-on get ${DIR} with project ${PROJNAME} in $(pwd)" >&3
Expand All @@ -130,9 +170,6 @@ teardown() {
assert_success
run ddev dotenv set .ddev/.env.adminer \
--adminer-default-driver=sqlite \
--adminer-default-username="" \
--adminer-default-password="" \
--adminer-default-server="" \
--adminer-default-db=/mnt/ddev_app/test.sqlite
assert_success
assert_file_exist .ddev/.env.adminer
Expand All @@ -144,7 +181,9 @@ teardown() {
# Create the test sqlite database
run ddev exec sqlite3 test.sqlite "CREATE TABLE items(id INTEGER PRIMARY KEY, name TEXT); INSERT INTO items(name) VALUES ('alpha'), ('beta');"
assert_success
# Specify the redirect target
export TEST_REDIRECT_LOCATION="?sqlite=&username=&db=%2Fmnt%2Fddev_app%2Ftest.sqlite"
# Specify the redirect target, the unused credentials are passed through
export TEST_REDIRECT_LOCATION="?sqlite=db&username=db&db=%2Fmnt%2Fddev_app%2Ftest.sqlite"
export TEST_PAGE_TITLE="Database: /mnt/ddev_app/test.sqlite - db - Adminer"
export TEST_SQLITE_TABLE=items
health_checks
}