Auth: Add HTTPS Reverse Proxy & Keycloak for OpenID Connect tests #782

This commit is contained in:
Michael Mayer 2021-11-02 13:20:38 +01:00
parent 55bee9871f
commit dd578b7142
27 changed files with 3093 additions and 149 deletions

View file

@ -26,9 +26,9 @@ dep: dep-tensorflow dep-js dep-go
build: generate build-js build-go
install: install-bin install-assets
test: test-js test-go
test-go: reset-test-databases run-test-go
test-api: reset-test-databases run-test-api
test-short: reset-test-databases run-test-short
test-go: reset-testdb run-test-go
test-api: reset-testdb run-test-api
test-short: reset-testdb run-test-short
acceptance-private-run-chromium: acceptance-private-restart acceptance-private acceptance-private-stop
acceptance-public-run-chromium: acceptance-restart acceptance acceptance-stop
acceptance-private-run-firefox: acceptance-private-restart acceptance-private-firefox acceptance-private-stop
@ -161,9 +161,8 @@ acceptance-private-firefox:
reset-mariadb:
$(info Resetting photoprism database...)
mysql < scripts/sql/reset-mariadb.sql
reset-test-databases:
$(info Resetting test databases...)
mysql < scripts/sql/init-test-databases.sql
reset-testdb:
$(info Removing test database files...)
find ./internal -type f -name '.test.*' -delete
run-test-short:
$(info Running short Go unit tests in parallel mode...)
@ -242,4 +241,4 @@ fmt-go:
go fmt ./pkg/... ./internal/... ./cmd/...
goimports -w pkg internal cmd
tidy:
go mod tidy
go mod tidy

View file

@ -1,6 +1,6 @@
version: '3.5'
## Legacy Databases Servers (for developers only)
## Legacy Database Servers for Development & Testing
services:
## MariaDB 10.5.5 Database Server
## affected by MDEV-25362: Incorrect name resolution for subqueries in ON expressions
@ -11,7 +11,7 @@ services:
expose:
- "4001" # Database port (internal)
volumes:
- "./scripts/sql/init-test-databases.sql:/docker-entrypoint-initdb.d/init-test-databases.sql"
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
environment:
MYSQL_ROOT_PASSWORD: photoprism
MYSQL_USER: photoprism
@ -25,7 +25,7 @@ services:
expose:
- "4001" # Database port (internal)
volumes:
- "./scripts/sql/init-test-databases.sql:/docker-entrypoint-initdb.d/init-test-databases.sql"
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
environment:
MYSQL_ROOT_PASSWORD: photoprism
MYSQL_USER: photoprism
@ -39,7 +39,7 @@ services:
expose:
- "4001" # Database port (internal)
volumes:
- "./scripts/sql/init-test-databases.sql:/docker-entrypoint-initdb.d/init-test-databases.sql"
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
environment:
MYSQL_ROOT_PASSWORD: photoprism
MYSQL_USER: photoprism
@ -53,7 +53,7 @@ services:
expose:
- "4001" # Database port (internal)
volumes:
- "./scripts/sql/init-test-databases.sql:/docker-entrypoint-initdb.d/init-test-databases.sql"
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
environment:
MYSQL_ROOT_PASSWORD: photoprism
MYSQL_USER: photoprism
@ -67,7 +67,7 @@ services:
expose:
- "4001" # Database port (internal)
volumes:
- "./scripts/sql/init-test-databases.sql:/docker-entrypoint-initdb.d/init-test-databases.sql"
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
environment:
MYSQL_ROOT_PASSWORD: photoprism
MYSQL_USER: photoprism

View file

@ -1,8 +1,9 @@
version: '3.5'
## Continuous integration environment (for Drone CI)
## Integration Environment for Drone CI
services:
## App Server
## App Dev Container
## Docs: https://docs.photoprism.org/developer-guide/
photoprism:
build: .
image: photoprism/photoprism:develop
@ -142,23 +143,24 @@ services:
DRONE_TARGET_BRANCH:
## MariaDB Database Server
## Docs: https://mariadb.com/docs/reference/cs10.6/
mariadb:
image: mariadb:10.6
command: mysqld --port=4001 --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
expose:
- "4001" # Database port (internal)
volumes:
- "./scripts/sql/init-test-databases.sql:/docker-entrypoint-initdb.d/init-test-databases.sql"
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
environment:
MYSQL_ROOT_PASSWORD: photoprism
MYSQL_USER: photoprism
MYSQL_PASSWORD: photoprism
MYSQL_DATABASE: photoprism
## Dummy WebDAV Server (for testing)
## Dummy WebDAV Server
dummy-webdav:
image: photoprism/dummy-webdav:20211022
## Dummy OpenID Connect Server (for testing)
## Dummy OpenID Connect Server
dummy-oidc:
image: photoprism/dummy-oidc:20211022

View file

@ -1,8 +1,9 @@
version: '3.5'
## Stable Release (for testing only)
## Latest Stable Release for QA
services:
## App Server
## Docs: https://docs.photoprism.org/
photoprism-latest:
image: photoprism/photoprism:latest
security_opt:
@ -10,10 +11,20 @@ services:
- apparmor:unconfined
ports:
- "2344:2342" # HTTP port (host:container)
labels:
- "traefik.enable=true"
- "traefik.http.services.photoprism-latest.loadbalancer.server.port=2342"
- "traefik.http.routers.photoprism-latest.entrypoints=websecure"
- "traefik.http.routers.photoprism-latest.rule=Host(`photoprism-latest.reverseproxy.dev`)"
- "traefik.http.routers.photoprism-latest.tls.domains[0].main=reverseproxy.dev"
- "traefik.http.routers.photoprism-latest.tls.domains[0].sans=*.reverseproxy.dev"
- "traefik.http.routers.photoprism-latest.tls=true"
environment:
PHOTOPRISM_UID: ${UID:-1000}
PHOTOPRISM_GID: ${GID:-1000}
PHOTOPRISM_SITE_URL: "http://localhost:2344/"
PHOTOPRISM_UID: ${UID:-1000} # User ID
PHOTOPRISM_GID: ${GID:-1000} # Group ID
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # Admin password (min 4 characters)
## Public server URL incl http:// or https:// and /path, :port is optional
PHOTOPRISM_SITE_URL: "https://photoprism-latest.reverseproxy.dev/"
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "Browse Your Life"
PHOTOPRISM_SITE_DESCRIPTION: "Open-Source Photo Management"
@ -28,10 +39,9 @@ services:
PHOTOPRISM_HTTP_COMPRESSION: "gzip" # Improves transfer speed and bandwidth utilization (none or gzip)
PHOTOPRISM_DATABASE_DRIVER: "mysql"
PHOTOPRISM_DATABASE_SERVER: "mariadb:4001"
PHOTOPRISM_DATABASE_NAME: "latest"
PHOTOPRISM_DATABASE_USER: "root"
PHOTOPRISM_DATABASE_PASSWORD: "photoprism"
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # The initial admin password (min 4 characters)
PHOTOPRISM_DATABASE_NAME: "photoprism_latest"
PHOTOPRISM_DATABASE_USER: "photoprism_latest"
PHOTOPRISM_DATABASE_PASSWORD: "photoprism_latest"
PHOTOPRISM_DISABLE_CHOWN: "false" # Disables storage permission updates on startup
PHOTOPRISM_DISABLE_BACKUPS: "false" # Don't backup photo and album metadata to YAML files
PHOTOPRISM_DISABLE_WEBDAV: "false" # Disables built-in WebDAV server

View file

@ -1,10 +1,16 @@
version: '3.5'
## For developers only! PostgreSQL is NOT supported yet as Gorm (the ORM library) needs to be
## upgraded first. The current version does NOT support compatible general data types:
## https://github.com/photoprism/photoprism/issues/47
# ATTENTION: PostgreSQL is NOT supported yet as Gorm (our ORM library) needs to be upgraded first.
# The current Gorm version does NOT support compatible general data types:
# https://github.com/photoprism/photoprism/issues/47
## Development Environment with
## - App Dev Container
## - PostgreSQL Database Server
## - and Dummy Services
services:
## App Server
## App Dev Container
## Docs: https://docs.photoprism.org/developer-guide/
photoprism:
build: .
image: photoprism/photoprism:develop
@ -23,7 +29,10 @@ services:
- "go-mod:/go/pkg/mod"
shm_size: "2gb"
environment:
PHOTOPRISM_SITE_URL: "http://localhost:2342/"
PHOTOPRISM_UID: ${UID:-1000} # User ID
PHOTOPRISM_GID: ${GID:-1000} # Group ID
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # Admin password (min 4 characters)
PHOTOPRISM_SITE_URL: "http://localhost:2342/" # Public server URL incl http:// or https:// and /path, :port is option
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "Browse Your Life"
PHOTOPRISM_SITE_DESCRIPTION: "Open-Source Photo Management"
@ -43,7 +52,6 @@ services:
PHOTOPRISM_DATABASE_PASSWORD: "photoprism"
PHOTOPRISM_TEST_DRIVER: "sqlite"
PHOTOPRISM_TEST_DSN: ".test.db"
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # The initial admin password (min 4 characters)
PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets"
PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage"
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
@ -67,6 +75,7 @@ services:
TF_CPP_MIN_LOG_LEVEL: 0 # Show TensorFlow log messages for development
## PostgreSQL Database Server
## Docs: https://www.postgresql.org/docs/
postgres:
image: postgres:12-alpine
ports:
@ -76,11 +85,11 @@ services:
POSTGRES_USER: photoprism
POSTGRES_PASSWORD: photoprism
## Dummy WebDAV Server (for testing)
## Dummy WebDAV Server
dummy-webdav:
image: photoprism/dummy-webdav:20211022
## Dummy OpenID Connect Server (for testing)
## Dummy OpenID Connect Server
dummy-oidc:
image: photoprism/dummy-oidc:20211022
# Expose port 9998 on host

View file

@ -1,21 +0,0 @@
version: '3.5'
## HTTP / HTTPS Reverse Proxy Servers (for developers only)
services:
## Caddy 2
caddy:
image: caddy:2
depends_on:
- photoprism
ports:
- "80:80" # HTTP port (host:container)
- "443:443" # HTTPS port (host:container)
volumes:
- ./docker/development/caddy:/data/caddy/pki/authorities/local
- ./docker/development/caddy/Caddyfile:/etc/caddy/Caddyfile
## Join shared network
networks:
default:
external:
name: shared

View file

@ -1,8 +1,24 @@
version: '3.5'
## Development environment with app server, database, and dummy services (for developers only)
## Development Environment with
## - HTTPS Reverse Proxy
## - App Dev Container
## - MariaDB Database Server
## - Keycloak OpenID Connect Provider
## - and Dummy Services
services:
## App Server
## Traefik 2.5 HTTPS Reverse Proxy (https://*.reverseproxy.dev/)
## Docs: https://doc.traefik.io/traefik/
traefik:
image: photoprism/traefik:20211102
ports:
# - "80:80" # HTTP (redirects to HTTPS)
- "443:443" # HTTPS (required)
volumes:
- "/var/run/docker.sock:/var/run/docker.sock" # Host names are configured with Docker labels
## App Dev Container
## Docs: https://docs.photoprism.org/developer-guide/
photoprism:
build: .
image: photoprism/photoprism:develop
@ -17,10 +33,25 @@ services:
- "2343:2343" # Acceptance Test HTTP port (host:container)
- "40000:40000" # Go Debugger (host:container)
shm_size: "2gb"
links:
- "traefik:keycloak.reverseproxy.dev"
- "traefik:photoprism.reverseproxy.dev"
- "traefik:dummy-webdav.reverseproxy.dev"
- "traefik:dummy-oidc.reverseproxy.dev"
labels:
- "traefik.enable=true"
- "traefik.http.services.photoprism.loadbalancer.server.port=2342"
- "traefik.http.routers.photoprism.entrypoints=websecure"
- "traefik.http.routers.photoprism.rule=Host(`photoprism.reverseproxy.dev`)"
- "traefik.http.routers.photoprism.tls.domains[0].main=reverseproxy.dev"
- "traefik.http.routers.photoprism.tls.domains[0].sans=*.reverseproxy.dev"
- "traefik.http.routers.photoprism.tls=true"
environment:
PHOTOPRISM_UID: ${UID:-1000}
PHOTOPRISM_GID: ${GID:-1000}
PHOTOPRISM_SITE_URL: "http://localhost:2342/"
PHOTOPRISM_UID: ${UID:-1000} # User ID
PHOTOPRISM_GID: ${GID:-1000} # Group ID
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # Admin password (min 4 characters)
## External development server URL incl http:// or https:// and /path, :port is optional
PHOTOPRISM_SITE_URL: "https://photoprism.reverseproxy.dev/"
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "Browse Your Life"
PHOTOPRISM_SITE_DESCRIPTION: "Open-Source Photo Management"
@ -40,7 +71,6 @@ services:
PHOTOPRISM_DATABASE_PASSWORD: "photoprism"
PHOTOPRISM_TEST_DRIVER: "sqlite"
PHOTOPRISM_TEST_DSN: ".test.db"
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # The initial admin password (min 4 characters)
PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets"
PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage"
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
@ -62,13 +92,11 @@ services:
PHOTOPRISM_THUMB_SIZE_UNCACHED: 7680 # On-demand rendering size limit (default 7680, min 720, max 7680)
PHOTOPRISM_JPEG_SIZE: 7680 # Size limit for converted image files in pixels (720-30000)
PHOTOPRISM_JPEG_QUALITY: 92 # Set to 95 for high-quality thumbnails (25-100)
PHOTOPRISM_OIDC_ISSUER: "https://keycloak.timovolkmann.de/auth/realms/master"
PHOTOPRISM_OIDC_CLIENT_ID: "photoprism-dev"
PHOTOPRISM_OIDC_CLIENT_SECRET: "9d8351a0-ca01-4556-9c37-85eb634869b9"
# PHOTOPRISM_OIDC_ISSUER: "https://accounts.google.com"
# PHOTOPRISM_OIDC_CLIENT_ID: "86720117204-mb09c300nas5r9rid1ad0omv67nlvhck.apps.googleusercontent.com"
# PHOTOPRISM_OIDC_CLIENT_SECRET: "WQ2-LdfhYhHd-BdpfZCYGE12"
TF_CPP_MIN_LOG_LEVEL: 0 # Show TensorFlow log messages for development
## OpenID Connect Provider (pre-configured for local Keycloak test server):
PHOTOPRISM_OIDC_ISSUER_URL: "https://keycloak.reverseproxy.dev/auth/realms/master"
PHOTOPRISM_OIDC_CLIENT_ID: "photoprism-development"
PHOTOPRISM_OIDC_CLIENT_SECRET: "9d8351a0-ca01-4556-9c37-85eb634869b9"
## Enable TensorFlow AVX2 support for modern Intel CPUs (requires starting the container as root):
# PHOTOPRISM_INIT: "tensorflow-amd64-avx2"
## Hardware video transcoding config (optional):
@ -89,6 +117,7 @@ services:
- "go-mod:/go/pkg/mod"
## MariaDB Database Server
## Docs: https://mariadb.com/docs/reference/cs10.6/
mariadb:
image: mariadb:10.6
command: mysqld --port=4001 --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
@ -97,30 +126,67 @@ services:
ports:
- "4001:4001" # Database port (host:container)
volumes:
- "./scripts/sql/init-test-databases.sql:/docker-entrypoint-initdb.d/init-test-databases.sql"
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
environment:
MYSQL_ROOT_PASSWORD: photoprism
MYSQL_USER: photoprism
MYSQL_PASSWORD: photoprism
MYSQL_DATABASE: photoprism
## Dummy WebDAV Server (for testing)
## Keycloak OpenID Connect Provider
## Docs: https://www.keycloak.org/getting-started/getting-started-docker
keycloak:
image: quay.io/keycloak/keycloak:15.0.2
links:
- "traefik:photoprism.reverseproxy.dev"
labels:
- "traefik.enable=true"
- "traefik.http.services.keycloak.loadbalancer.server.port=8080"
- "traefik.http.routers.keycloak.entrypoints=websecure"
- "traefik.http.routers.keycloak.rule=Host(`keycloak.reverseproxy.dev`)"
- "traefik.http.routers.keycloak.tls.domains[0].main=reverseproxy.dev"
- "traefik.http.routers.keycloak.tls.domains[0].sans=*.reverseproxy.dev"
- "traefik.http.routers.keycloak.tls=true"
environment:
KEYCLOAK_USER: "admin"
KEYCLOAK_PASSWORD: "photoprism"
KEYCLOAK_FRONTEND_URL: "https://keycloak.reverseproxy.dev/auth"
DB_VENDOR: "mariadb"
DB_PORT: 4001
DB_DATABASE: "keycloak"
DB_USER: "keycloak"
DB_PASSWORD: "keycloak"
## Dummy WebDAV Server
dummy-webdav:
image: photoprism/dummy-webdav:20211022
labels:
- "traefik.enable=true"
- "traefik.http.services.dummy-webdav.loadbalancer.server.port=80"
- "traefik.http.routers.dummy-webdav.entrypoints=websecure"
- "traefik.http.routers.dummy-webdav.rule=Host(`dummy-webdav.reverseproxy.dev`)"
- "traefik.http.routers.dummy-webdav.tls.domains[0].main=reverseproxy.dev"
- "traefik.http.routers.dummy-webdav.tls.domains[0].sans=*.reverseproxy.dev"
- "traefik.http.routers.dummy-webdav.tls=true"
## Dummy OpenID Connect Server (for testing)
## Dummy OpenID Connect Server
dummy-oidc:
image: photoprism/dummy-oidc:20211022
# Expose port 9998 on host
# ports:
# - "9998:9998"
labels:
- "traefik.enable=true"
- "traefik.http.services.dummy-oidc.loadbalancer.server.port=9998"
- "traefik.http.routers.dummy-oidc.entrypoints=websecure"
- "traefik.http.routers.dummy-oidc.rule=Host(`dummy-oidc.reverseproxy.dev`)"
- "traefik.http.routers.dummy-oidc.tls.domains[0].main=reverseproxy.dev"
- "traefik.http.routers.dummy-oidc.tls.domains[0].sans=*.reverseproxy.dev"
- "traefik.http.routers.dummy-oidc.tls=true"
## Create named volumes
## Create named volume for Go module cache.
volumes:
go-mod:
driver: local
## Create shared network
## Create shared network for connecting with services in other docker-compose.yml files.
networks:
default:
name: shared

View file

@ -1,2 +0,0 @@
*.crt
*.key

View file

@ -1,3 +0,0 @@
*, localhost {
reverse_proxy photoprism:2342
}

View file

@ -0,0 +1,71 @@
{
"clientId": "photoprism-development",
"secret": "9d8351a0-ca01-4556-9c37-85eb634869b9",
"name": "PhotoPrism",
"rootUrl": "https://photoprism.reverseproxy.dev/",
"adminUrl": "https://photoprism.reverseproxy.dev/",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"https://photoprism.reverseproxy.dev/api/v1/auth/callback"
],
"webOrigins": [
"https://photoprism.reverseproxy.dev"
],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": false,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"id.token.as.detached.signature": "false",
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"use.refresh.tokens": "true",
"exclude.session.state.from.auth.response": "false",
"oidc.ciba.grant.enabled": "false",
"saml.artifact.binding": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"require.pushed.authorization.requests": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}

View file

@ -0,0 +1,75 @@
{
"clients": [
{
"clientId": "photoprism-development",
"secret": "9d8351a0-ca01-4556-9c37-85eb634869b9",
"name": "PhotoPrism",
"rootUrl": "https://photoprism.reverseproxy.dev/",
"adminUrl": "https://photoprism.reverseproxy.dev/",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"https://photoprism.reverseproxy.dev/api/v1/auth/callback"
],
"webOrigins": [
"https://photoprism.reverseproxy.dev"
],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": false,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"id.token.as.detached.signature": "false",
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"use.refresh.tokens": "true",
"exclude.session.state.from.auth.response": "false",
"oidc.ciba.grant.enabled": "false",
"saml.artifact.binding": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"require.pushed.authorization.requests": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}
]
}

View file

@ -6,8 +6,8 @@ SOFTWARE INCLUDED
PhotoPrism latest, AGPL 3
Docker CE 20.10, Apache 2
Traefik 2.4, MIT
MariaDB 10.5, GPL 2
Traefik 2.5, MIT
MariaDB 10.6, GPL 2
Ofelia 0.3.4, MIT
Watchtower 1.3, Apache 2

View file

@ -15,8 +15,8 @@ SOFTWARE INCLUDED
- [PhotoPrism latest](https://docs.photoprism.org/release-notes/), AGPL 3
- [Docker CE 20.10](https://docs.docker.com/engine/release-notes/), Apache 2
- [Traefik 2.4](https://github.com/traefik/traefik/releases), MIT
- [MariaDB 10.5](https://mariadb.com/kb/en/release-notes/), GPL 2
- [Traefik 2.5](https://github.com/traefik/traefik/releases), MIT
- [MariaDB 10.6](https://mariadb.com/kb/en/release-notes/), GPL 2
- [Ofelia 0.3.4](https://github.com/mcuadros/ofelia/releases), MIT
- [Watchtower 1.3](https://github.com/containrrr/watchtower/releases), Apache 2

View file

@ -171,7 +171,7 @@ services:
## see https://docs.photoprism.org/getting-started/proxies/traefik/
traefik:
restart: always
image: traefik:v2.4
image: traefik:v2.5
container_name: traefik
ports:
- "80:80"

10
go.mod
View file

@ -2,7 +2,7 @@ module github.com/photoprism/photoprism
require (
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/caos/oidc v0.15.11
github.com/caos/oidc v0.15.12
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/disintegration/imaging v1.6.2
github.com/djherbis/times v1.5.0
@ -60,8 +60,8 @@ require (
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 // indirect
golang.org/x/net v0.0.0-20211020060615-d418f374d309
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect
gonum.org/v1/gonum v0.9.3
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/photoprism/go-tz.v2 v2.1.1
@ -90,9 +90,9 @@ require (
github.com/tidwall/match v1.0.3 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/ugorji/go/codec v1.2.6 // indirect
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/appengine v1.6.6 // indirect
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)

12
go.sum
View file

@ -41,10 +41,10 @@ github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhP
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/caos/logging v0.0.2/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0=
github.com/caos/oidc v0.15.10 h1:dSzkIvsZR2PSZgvBFFkLJt8A/MujsyLac1yNvBShXuw=
github.com/caos/oidc v0.15.10/go.mod h1:4l0PPwdc6BbrdCFhNrRTUddsG292uHGa7gE2DSEIqoU=
github.com/caos/oidc v0.15.11 h1:NVhdUte5a3u6x65VARceY2mG2WOgxUcqlZunvoeTe0c=
github.com/caos/oidc v0.15.11/go.mod h1:4l0PPwdc6BbrdCFhNrRTUddsG292uHGa7gE2DSEIqoU=
github.com/caos/oidc v0.15.12 h1:vBVOsQlFCfW7fV43acxkg2V0NHh0NWA4lWzWiJ6LgOk=
github.com/caos/oidc v0.15.12/go.mod h1:4l0PPwdc6BbrdCFhNrRTUddsG292uHGa7gE2DSEIqoU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@ -468,6 +468,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6 h1:Z04ewVs7JhXaYkmDhBERPi41gnltfQpMWDnTnQbaCqk=
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211020060615-d418f374d309 h1:A0lJIi+hcTR6aajJH4YqKWwohY4aW9RO7oRMcdv+HKI=
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -475,6 +477,8 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 h1:B333XXssMuKQeBwiNODx4TupZy7bf4sxFZnN2ZOcvUE=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -525,6 +529,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 h1:J27LZFQBFoihqXoegpscI10HpjZ7B5WQLLKL2FZXQKw=
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -619,6 +625,8 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=

View file

@ -496,21 +496,21 @@ var GlobalFlags = []cli.Flag{
EnvVar: "PHOTOPRISM_FACE_MATCH_DIST",
},
cli.StringFlag{
Name: "client-id",
Name: "oidc-client-id",
Usage: "OpenID Connect Client ID",
Value: "",
EnvVar: "PHOTOPRISM_OIDC_CLIENT_ID",
},
cli.StringFlag{
Name: "client-secret",
Name: "oidc-client-secret",
Usage: "OpenID Connect Client Secret",
Value: "",
EnvVar: "PHOTOPRISM_OIDC_CLIENT_SECRET",
},
cli.StringFlag{
Name: "oidc-issuer",
Usage: "OpenID Connect Provider/Issuer URL",
Name: "oidc-issuer-url",
Usage: "OpenID Connect Issuer URL",
Value: "",
EnvVar: "PHOTOPRISM_OIDC_ISSUER",
EnvVar: "PHOTOPRISM_OIDC_ISSUER_URL",
},
}

View file

@ -3,10 +3,10 @@ package config
import "net/url"
func (c *Config) OidcIssuerUrl() *url.URL {
if c.Options().OidcIssuer == "" {
if c.Options().OidcIssuerUrl == "" {
return nil
}
res, err := url.Parse(c.Options().OidcIssuer)
res, err := url.Parse(c.Options().OidcIssuerUrl)
if err != nil {
log.Debugf("error parsing oidc issuer url: %q", err)
return nil

View file

@ -21,13 +21,10 @@ const (
Postgres = "postgres" // TODO: Requires GORM 2.0 for generic column data types
)
// Options provides a struct in which application configuration is stored.
// Application code must use functions to get config options, for two reasons:
//
// 1. Some options are computed and we don't want to leak implementation details (aims at reducing refactoring overhead).
//
// 2. Paths might actually be dynamic later (if we build a multi-user version).
//
// Options provides a struct in which the application configuration is stored.
// The application code must use functions to obtain configuration values:
// 1. We don't want to leak implementation details to reduce refactoring overhead
// 2. Path names can be dynamic later, for example depending on the user ID
// See https://github.com/photoprism/photoprism/issues/50#issuecomment-433856358
type Options struct {
Name string `json:"-"`
@ -42,6 +39,9 @@ type Options struct {
Sponsor bool `yaml:"-" json:"-" flag:"sponsor"`
Public bool `yaml:"Public" json:"-" flag:"public"`
AdminPassword string `yaml:"AdminPassword" json:"-" flag:"admin-password"`
OidcClientID string `yaml:"-" json:"-" flag:"oidc-client-id"`
OidcClientSecret string `yaml:"-" json:"-" flag:"oidc-client-secret"`
OidcIssuerUrl string `yaml:"-" json:"-" flag:"oidc-issuer-url"`
ReadOnly bool `yaml:"ReadOnly" json:"ReadOnly" flag:"read-only"`
Experimental bool `yaml:"Experimental" json:"Experimental" flag:"experimental"`
ConfigPath string `yaml:"ConfigPath" json:"-" flag:"config-path"`
@ -123,9 +123,6 @@ type Options struct {
FaceClusterDist float64 `yaml:"-" json:"-" flag:"face-cluster-dist"`
FaceMatchDist float64 `yaml:"-" json:"-" flag:"face-match-dist"`
PIDFilename string `yaml:"PIDFilename" json:"-" flag:"pid-filename"`
OidcClientID string `yaml:"OidcClientID" json:"-" flag:"client-id"`
OidcClientSecret string `yaml:"OidcClientSecret" json:"-" flag:"client-secret"`
OidcIssuer string `yaml:"OidcIssuer" json:"-" flag:"oidc-issuer"`
}
// NewOptions creates a new configuration entity by using two methods:

View file

@ -74,7 +74,7 @@ func NewTestOptions() *Options {
DatabaseDriver: dbDriver,
DatabaseDsn: dbDsn,
AdminPassword: "photoprism",
OidcIssuer: "http://dummy-oidc:9998",
OidcIssuerUrl: "http://dummy-oidc:9998",
OidcClientID: "native",
OidcClientSecret: "random",
}

View file

@ -1,8 +1,9 @@
package form
import (
"github.com/stretchr/testify/assert"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewFaceSearch(t *testing.T) {

View file

@ -27,21 +27,28 @@ type Client struct {
debug bool
}
func NewClient(iss *url.URL, clientId, clientSecret, siteUrl string, debug bool) (*Client, error) {
log.Debugf("Provider Params: %s %s %s %s", iss.String(), clientId, clientSecret, siteUrl)
func NewClient(iss *url.URL, clientId, clientSecret, siteUrl string, debug bool) (result *Client, err error) {
log.Debugf("oidc: Provider Params: %s %s %s %s", iss.String(), clientId, clientSecret, siteUrl)
u, err := url.Parse(siteUrl)
if err != nil {
log.Error(err)
return nil, err
}
u.Path = path.Join(u.Path, "/api/v1/", RedirectPath)
log.Debugf(u.String())
hashKey, err := rnd.RandomBytes(16)
encryptKey, err := rnd.RandomBytes(16)
if err != nil {
log.Errorf("oidc intialization: %q", err)
u.Path = path.Join(u.Path, "/api/v1/", RedirectPath)
log.Debugf("oidc: %s", u.String())
var hashKey, encryptKey []byte
if hashKey, err = rnd.RandomBytes(16); err != nil {
log.Errorf("oidc: %q (create hash key)", err)
return nil, err
}
if encryptKey, err = rnd.RandomBytes(16); err != nil {
log.Errorf("oidc: %q (create encrypt key)", err)
return nil, err
}
@ -57,10 +64,12 @@ func NewClient(iss *url.URL, clientId, clientSecret, siteUrl string, debug bool)
}
discover, err := client.Discover(iss.String(), httpClient)
if err != nil {
log.Errorf("oidc intialization: %q", err)
log.Errorf("oidc: %q (discover)", err)
return nil, err
}
for _, v := range discover.CodeChallengeMethodsSupported {
if v == oidc.CodeChallengeMethodS256 {
options = append(options, rp.WithPKCE(cookieHandler))
@ -70,11 +79,13 @@ func NewClient(iss *url.URL, clientId, clientSecret, siteUrl string, debug bool)
scopes := strings.Split("openid profile email", " ")
provider, err := rp.NewRelyingPartyOIDC(iss.String(), clientId, clientSecret, u.String(), scopes, options...)
if err != nil {
log.Errorf("oidc intialization: %s", err)
log.Errorf("oidc: %s (issuer)", err)
return nil, err
}
log.Debugf("PKCE enabled: %v", provider.IsPKCE())
log.Debugf("oidc: PKCE enabled %v", provider.IsPKCE())
return &Client{
provider,
@ -94,10 +105,10 @@ func (c *Client) CodeExchangeUserInfo(ctx *gin.Context) (oidc.UserInfo, error) {
var userinfo oidc.UserInfo
userinfoClosure := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty, info oidc.UserInfo) {
log.Infof("UserInfo: %s %s %s %s %s", info.GetEmail(), info.GetSubject(), info.GetNickname(), info.GetName(), info.GetPreferredUsername())
log.Debugf("IDToken: %s", tokens.IDToken)
log.Debugf("AToken: %s", tokens.AccessToken)
log.Debugf("RToken: %s", tokens.RefreshToken)
log.Infof("oidc: UserInfo: %s %s %s %s %s", info.GetEmail(), info.GetSubject(), info.GetNickname(), info.GetName(), info.GetPreferredUsername())
log.Debugf("oidc: IDToken: %s", tokens.IDToken)
log.Debugf("oidc: AToken: %s", tokens.AccessToken)
log.Debugf("oidc: RToken: %s", tokens.RefreshToken)
userinfo = info
}
@ -114,7 +125,7 @@ func (c *Client) CodeExchangeUserInfo(ctx *gin.Context) (oidc.UserInfo, error) {
//handle := rp.CodeExchangeHandler(tokeninfoClosure, c)
handle(ctx.Writer, ctx.Request)
log.Debugf("current request state: %v", ctx.Writer.Status())
log.Debugf("oidc: current request state: %v", ctx.Writer.Status())
if sc := ctx.Writer.Status(); sc != 0 && sc != http.StatusOK {
return nil, errors.New("oidc: couldn't exchange auth code and thus not retrieve external user info")
}

View file

@ -1,8 +1,9 @@
package thumb
import (
"github.com/stretchr/testify/assert"
"testing"
"github.com/stretchr/testify/assert"
)
func TestName_Jpeg(t *testing.T) {

34
scripts/mariadb-init.sh Executable file
View file

@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Create default databases
cat << EOF
CREATE DATABASE IF NOT EXISTS keycloak;
CREATE USER IF NOT EXISTS keycloak@'%' IDENTIFIED BY 'keycloak';
GRANT ALL PRIVILEGES ON keycloak.* TO keycloak@'%';
CREATE DATABASE IF NOT EXISTS photoprism_latest;
CREATE USER IF NOT EXISTS photoprism_latest@'%' IDENTIFIED BY 'photoprism_latest';
GRANT ALL PRIVILEGES ON photoprism_latest.* TO photoprism_latest@'%';
CREATE DATABASE IF NOT EXISTS photoprism_preview;
CREATE USER IF NOT EXISTS photoprism_preview@'%' IDENTIFIED BY 'photoprism_preview';
GRANT ALL PRIVILEGES ON photoprism_preview.* TO photoprism_preview@'%';
CREATE DATABASE IF NOT EXISTS acceptance;
CREATE USER IF NOT EXISTS acceptance@'%' IDENTIFIED BY 'acceptance';
GRANT ALL PRIVILEGES ON acceptance.* TO acceptance@'%';
EOF
# Create additional test databases
for USER_ID in $(seq -f "%02g" 1 5)
do
echo "CREATE DATABASE IF NOT EXISTS photoprism_$USER_ID;"
echo "CREATE USER IF NOT EXISTS photoprism_$USER_ID@'%' IDENTIFIED BY 'photoprism_$USER_ID';";
echo "GRANT ALL PRIVILEGES ON photoprism_$USER_ID.* TO photoprism_$USER_ID@'%';"
done
cat << EOF
FLUSH PRIVILEGES;
EOF

View file

@ -1,21 +0,0 @@
CREATE DATABASE IF NOT EXISTS alpha;
CREATE DATABASE IF NOT EXISTS beta;
CREATE DATABASE IF NOT EXISTS gamma;
CREATE DATABASE IF NOT EXISTS latest;
CREATE DATABASE IF NOT EXISTS preview;
DROP DATABASE IF EXISTS acceptance;
CREATE DATABASE IF NOT EXISTS acceptance;
DROP DATABASE IF EXISTS api;
CREATE DATABASE IF NOT EXISTS api;
DROP DATABASE IF EXISTS config;
CREATE DATABASE IF NOT EXISTS config;
DROP DATABASE IF EXISTS entity;
CREATE DATABASE IF NOT EXISTS entity;
DROP DATABASE IF EXISTS query;
CREATE DATABASE IF NOT EXISTS query;
DROP DATABASE IF EXISTS remote;
CREATE DATABASE IF NOT EXISTS remote;
DROP DATABASE IF EXISTS service;
CREATE DATABASE IF NOT EXISTS service;
DROP DATABASE IF EXISTS workers;
CREATE DATABASE IF NOT EXISTS workers;

2700
scripts/sql/mariadb-init.sql Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,9 @@
DROP DATABASE IF EXISTS photoprism;
CREATE DATABASE IF NOT EXISTS photoprism;
DROP DATABASE IF EXISTS acceptance;
CREATE DATABASE IF NOT EXISTS acceptance;
CREATE USER IF NOT EXISTS acceptance@'%' IDENTIFIED BY 'acceptance';
GRANT ALL PRIVILEGES ON acceptance.* TO acceptance@'%';
FLUSH PRIVILEGES;