Skip to content
Open
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@
.bundle/*
./local/*
.DS_Store
.env

/data/*
!/data/gingr
/data/gingr/ready/*
/data/gingr/processed/*
/data/gingr/failed/*
/data/gingr/processing/*

!/data/geoserver/
/data/geoserver/*
!/data/geoserver/keycloak/

/solr/geodata-test/data
/solr/geodata-test/data
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ Command-line tool for ingesting GeoData (Solr, GeoServer, and file servers).
The app is Dockerized with an image designed to just run the `gingr` executable:

```sh
# set your env and then make any changes necessary.
cp .env.example .env

# Build the image
docker compose build

# The geoserver-config will need to run every time unless you mount the geoserver_data volume
# and comment out the geoserver-config service.
docker compose up -d

# Use `docker compose watch` to automatically rebuild on changes. This takes over the terminal
# you run it in, so you'll need to either background it (`screen` works great) or open a different window.
docker compose watch
Expand Down
83 changes: 83 additions & 0 deletions bin/geoserver-oidc-config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env sh
set -eu

GEOSERVER_AUTH="${GEOSERVER_AUTH:-admin:geoserver}"

# This script assumes both geoserver and geoserver-secure are healthy
# and configured exactly the same.
for GEOSERVER_HOST in geoserver geoserver-secure; do
GEOSERVER_REST="http://${GEOSERVER_HOST}:8080/geoserver/rest"

echo "Creating keycloak OIDC auth filter on ${GEOSERVER_HOST}..."
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
--request POST \
--url "${GEOSERVER_REST}/security/authfilters" \
--user "${GEOSERVER_AUTH}" \
--header 'Content-Type: application/xml; charset=utf-8' \
--data "<org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig>
<name>keycloak</name>
<className>org.geoserver.security.oauth2.login.GeoServerOAuth2LoginAuthenticationFilter</className>
<roleSource class=\"org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig\$OpenIdRoleSource\">IdToken</roleSource>
<baseRedirectUri>http://localhost:8080/geoserver/</baseRedirectUri>
<googleEnabled>false</googleEnabled>
<googleUserNameAttribute>email</googleUserNameAttribute>
<googleRedirectUri>http://localhost:8080/geoserver/web/login/oauth2/code/google</googleRedirectUri>
<gitHubEnabled>false</gitHubEnabled>
<gitHubUserNameAttribute>id</gitHubUserNameAttribute>
<gitHubRedirectUri>http://localhost:8080/geoserver/web/login/oauth2/code/gitHub</gitHubRedirectUri>
<msEnabled>false</msEnabled>
<msUserNameAttribute>sub</msUserNameAttribute>
<msRedirectUri>http://localhost:8080/geoserver/web/login/oauth2/code/microsoft</msRedirectUri>
<msScopes>openid profile email</msScopes>
<oidcEnabled>true</oidcEnabled>
<oidcClientId>${OIDC_CLIENT_ID}</oidcClientId>
<oidcClientSecret>${OIDC_CLIENT_SECRET}</oidcClientSecret>
<oidcUserNameAttribute>email</oidcUserNameAttribute>
<oidcRedirectUri>http://localhost:8080/geoserver/web/login/oauth2/code/oidc</oidcRedirectUri>
<oidcScopes>openid berkeley_edu_groups email profile</oidcScopes>
<oidcDiscoveryUri>http://keycloak:8180/realms/berkeley-local/.well-known/openid-configuration</oidcDiscoveryUri>
<oidcTokenUri>http://keycloak:8180/realms/berkeley-local/protocol/openid-connect/token</oidcTokenUri>
<oidcAuthorizationUri>http://keycloak.localhost:8180/realms/berkeley-local/protocol/openid-connect/auth</oidcAuthorizationUri>
<oidcUserInfoUri>http://keycloak:8180/realms/berkeley-local/protocol/openid-connect/userinfo</oidcUserInfoUri>
<oidcJwkSetUri>http://keycloak:8180/realms/berkeley-local/protocol/openid-connect/certs</oidcJwkSetUri>
<oidcLogoutUri>http://keycloak.localhost:8180/realms/berkeley-local/protocol/openid-connect/logout</oidcLogoutUri>
<oidcForceAuthorizationUriHttps>false</oidcForceAuthorizationUriHttps>
<oidcForceTokenUriHttps>false</oidcForceTokenUriHttps>
<oidcEnforceTokenValidation>true</oidcEnforceTokenValidation>
<oidcUsePKCE>false</oidcUsePKCE>
<oidcAuthenticationMethodPostSecret>false</oidcAuthenticationMethodPostSecret>
<oidcAllowUnSecureLogging>false</oidcAllowUnSecureLogging>
<tokenRolesClaim>preferred_username</tokenRolesClaim>
<postLogoutRedirectUri>http://localhost:8080/geoserver/web/</postLogoutRedirectUri>
<enableRedirectAuthenticationEntryPoint>true</enableRedirectAuthenticationEntryPoint>
<msGraphMemberOf>false</msGraphMemberOf>
<msGraphAppRoleAssignments>false</msGraphAppRoleAssignments>
<roleConverterString>testadmin=ADMIN</roleConverterString>
<onlyExternalListedRoles>false</onlyExternalListedRoles>
</org.geoserver.security.oauth2.login.GeoServerOAuth2LoginFilterConfig>")

case "$HTTP_STATUS" in
200|201) echo "Auth filter created." ;;
*) echo "ERROR: Unexpected status creating auth filter on ${GEOSERVER_HOST}: $HTTP_STATUS"; exit 1 ;;
esac

echo "Updating web filter chain on ${GEOSERVER_HOST}..."
FILTER_CHAIN_UPDATE_HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
--request PUT \
--url "${GEOSERVER_REST}/security/filterchain/web" \
--user "${GEOSERVER_AUTH}" \
--header 'Content-Type: application/xml; charset=utf-8' \
--data '<filters name="web" class="org.geoserver.security.HtmlLoginFilterChain" path="/web/**,/gwc/rest/web/**,/" disabled="false" allowSessionCreation="true" ssl="false" matchHTTPMethod="false" interceptorName="interceptor" exceptionTranslationName="exception">
<filter>rememberme</filter>
<filter>form</filter>
<filter>keycloak</filter>
<filter>anonymous</filter>
</filters>')

case "$FILTER_CHAIN_UPDATE_HTTP_STATUS" in
200) echo "Web filter chain updated." ;;
*) echo "ERROR: Unexpected status updating web filter chain: $FILTER_CHAIN_UPDATE_HTTP_STATUS"; exit 1 ;;
esac

echo "Done with ${GEOSERVER_HOST}."
done
102 changes: 102 additions & 0 deletions data/geoserver/keycloak/config/berkeley-local.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# keycloak-config-cli realm configuration for local GeoServer development.
# It provides a local Keycloak realm, GeoServer client, and Berkeley-style
# group claims for predictable OIDC testing.
#
# Group names mirror the production LDAP DN format so GeoServer can test the same
# role-mapping shape used against the real IdP.
#
# Secrets are injected at import time via substitution by keycloak-config-cli.
# Set OIDC_CLIENT_SECRET in your .env file — never commit a real secret here.

realm: berkeley-local
enabled: true
displayName: Berkeley Local

clientScopes:
- name: berkeley_edu_groups
description: Berkeley group memberships
protocol: openid-connect
attributes:
include.in.token.scope: "true"
display.on.consent.screen: "false"
protocolMappers:
- name: groups
protocol: openid-connect
protocolMapper: oidc-group-membership-mapper
consentRequired: false
config:
# Match Calnet's behavior by not prepending groups with "/"
full.path: "false"
id.token.claim: "true"
access.token.claim: "true"
introspection.token.claim: "true"
userinfo.token.claim: "true"
claim.name: groups
jsonType.label: String

clients:
- clientId: "$(OIDC_CLIENT_ID)"
name: "$(OIDC_CLIENT_ID)"
enabled: true
rootUrl: "http://localhost:8080"
adminUrl: "http://localhost:8080"
redirectUris:
- "http://localhost:8080/*"
webOrigins:
- "http://localhost:8080"
publicClient: false
secret: "$(OIDC_CLIENT_SECRET)"
standardFlowEnabled: true
directAccessGrantsEnabled: true
serviceAccountsEnabled: true
frontchannelLogout: true
attributes:
backchannel.logout.session.required: "true"
frontchannel.logout.session.required: "true"
defaultClientScopes:
- profile
- email
- berkeley_edu_groups

groups:
- name: "$(OIDC_ADMIN_GROUP)"
- name: "$(OIDC_USER_GROUP)"
- name: "cn=edu:berkeley:org:libr:unrelated:group,ou=campus groups,dc=berkeley,dc=edu"

users:
- username: testadmin
enabled: true
email: testadmin@example.edu
firstName: Test
lastName: Admin
credentials:
- type: password
value: testadmin
temporary: false
groups:
- "$(OIDC_ADMIN_GROUP)"
- "$(OIDC_USER_GROUP)"

- username: testuser
enabled: true
email: testuser@example.edu
firstName: Test
lastName: User
credentials:
- type: password
value: testuser
temporary: false
groups:
- "$(OIDC_USER_GROUP)"

- username: testpublic
enabled: true
email: testpublic@example.edu
firstName: Test
lastName: Public
credentials:
- type: password
value: testpublic
temporary: false
groups:
- cn=edu:berkeley:org:libr:unrelated:group,ou=campus groups,dc=berkeley,dc=edu
8 changes: 8 additions & 0 deletions docker-compose.ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,13 @@ services:

geoserver-secure: !reset

geoserver-config: !reset

postgres: !reset

keycloak: !reset

keycloak-config: !reset

volumes:
artifacts:
120 changes: 115 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ services:
condition: service_healthy
geoserver-secure:
condition: service_healthy
keycloak-config:
condition: service_completed_successfully
command:
- tail
- -f
Expand All @@ -25,7 +27,7 @@ services:
SPATIAL_URL: http://spatial
restart: no
volumes:
- ./:/opt/app:delegated
- ./:/opt/app

solr:
command: >
Expand All @@ -51,27 +53,135 @@ services:
- ./data/spatial:/usr/local/apache2/htdocs/:ro

geoserver:
image: ghcr.io/berkeleylibrary/geoserver:latest
image: docker.osgeo.org/geoserver:2.28-latest
ports:
- 8080:8080
environment: &geoserver-environment
ROOT_WEBAPP_REDIRECT: "true"
SKIP_DEMO_DATA: "true"
INSTALL_EXTENSIONS: "true"
COMMUNITY_EXTENSIONS: "sec-oidc"
PROXY_BASE_URL:
depends_on:
keycloak-config:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/geoserver/web/"]
interval: 15s
timeout: 10s
retries: 10
start_period: 30s
extra_hosts:
- keycloak.localhost:host-gateway
volumes:
- ./data/geoserver/public:/srv/geofiles:delegated
# Uncomment and comment out the geoserver-config service to persist geoserver between runs.
# - ./data/geoserver/geoserver_data:/opt/geoserver_data
Comment thread
jason-raitz marked this conversation as resolved.
- ./data/geoserver/public:/srv/geofiles

geoserver-secure:
image: ghcr.io/berkeleylibrary/geoserver:latest
image: docker.osgeo.org/geoserver:2.28-latest
ports:
- 8081:8080
environment: *geoserver-environment
depends_on:
keycloak-config:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/geoserver/web/"]
interval: 15s
timeout: 10s
retries: 10
start_period: 30s
extra_hosts:
- keycloak.localhost:host-gateway
volumes:
- ./data/geoserver/UCB:/srv/geofiles

geoserver-config:
image: curlimages/curl:latest
depends_on:
geoserver:
condition: service_healthy
geoserver-secure:
condition: service_healthy
entrypoint: ["/bin/sh"]
command: ["/scripts/geoserver-oidc-config.sh"]
environment:
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID}
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET}
restart: on-failure:3
volumes:
- ./bin/geoserver-oidc-config.sh:/scripts/geoserver-oidc-config.sh:ro

postgres:
image: postgres:18-alpine
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: keycloak
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- ./data/geoserver/UCB:/srv/geofiles:delegated
- postgres_data:/var/lib/postgresql

keycloak:
command: start-dev
depends_on:
postgres:
condition: service_healthy
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: keycloak
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
KC_HOSTNAME: "http://keycloak.localhost:8180"
KC_HOSTNAME_STRICT: "false"
KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true"
KC_HTTP_ENABLED: "true"
KC_HTTP_PORT: "8180"
KC_HEALTH_ENABLED: "true"
KC_LOG_LEVEL: ${KC_LOG_LEVEL:-INFO}
healthcheck:
# curl/wget are not present in the UBI9-based Keycloak image; use /dev/tcp.
# KC_HEALTH_ENABLED=true exposes /health/ready on the management port (9000).
test:
[
"CMD-SHELL",
"exec 3<>/dev/tcp/localhost/9000 && printf 'GET /health/ready HTTP/1.0\\r\\nHost: localhost\\r\\n\\r\\n' >&3 && grep -q 'status.*UP' <&3",
]
interval: 30s
timeout: 10s
retries: 5
start_period: 20s
image: quay.io/keycloak/keycloak:26.1
ports:
- 127.0.0.1:8180:8180
restart: always

keycloak-config:
depends_on:
keycloak:
condition: service_healthy
environment:
KEYCLOAK_URL: http://keycloak:8180
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: admin
KEYCLOAK_AVAILABILITYCHECK_ENABLED: "true"
IMPORT_FILES_LOCATIONS: /config/*
IMPORT_VAR_SUBSTITUTION_ENABLED: "true"
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID}
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET}
OIDC_ADMIN_GROUP: ${OIDC_ADMIN_GROUP}
OIDC_USER_GROUP: ${OIDC_USER_GROUP}
image: adorsys/keycloak-config-cli:latest-26.1.0
restart: on-failure:3
volumes:
- ./data/geoserver/keycloak/config:/config

volumes:
postgres_data:
Loading
Loading