Working with avatars extension
parent
91d8a620d6
commit
0323112f4f
|
@ -0,0 +1,4 @@
|
|||
.env
|
||||
**/.env
|
||||
main.conf
|
||||
docker-compose.yml
|
|
@ -0,0 +1,20 @@
|
|||
# IsardVDI OpenID infrastructure
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [alpha1] - not released
|
||||
|
||||
### Added
|
||||
|
||||
- Auth containers: freeipa, mokey, hydra
|
||||
- Configuration file: main.conf
|
||||
- Build docker-compose yml: build.sh
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
### Removed
|
||||
|
165
README.md
165
README.md
|
@ -0,0 +1,165 @@
|
|||
# IsardVDI - OpenID infrastructure
|
||||
|
||||
This will bring up a full OpenID auth infrastructure consisting in this hosts from $DOMAIN var set in main.conf:
|
||||
|
||||
- FreeIPA: https://ipa.$DOMAIN
|
||||
- Mokey: https://login.$DOMAIN
|
||||
- Hydra: https://hydra.$DOMAIN
|
||||
|
||||
NOTE: If you use the default example domain in main.conf.example you will need to add this domain mapping to IP in your hosts file at clients.
|
||||
|
||||
# Quick start
|
||||
|
||||
1. Edit main.conf (to suit your needs). If you set Letsencrypt vars it will ask and renew the certificate automatically.
|
||||
2. Build docker.compose.yml: ```./build-compose.sh```
|
||||
3. Bring containers up: ```docker-compose up -d```
|
||||
|
||||
Wait till freeipa container is ready (it can take SEVERAL minutes the first time): ```docker logs freeipa --follow```
|
||||
|
||||
And wait to be ready. It will log: *FreeIPA server configured.*
|
||||
|
||||
4. Add mokey client in freeipa: ```docker exec freeipa /bin/sh -c "scripts/mokey.sh"```
|
||||
5. check that mokey is up: ```docker logs mokey --follow```. It will log: *⇨ http server started on [::]:8080*
|
||||
|
||||
Access your IP/DNS and login page should come up.
|
||||
|
||||
# Firewall
|
||||
|
||||
You should open 80 and 443
|
||||
|
||||
# Add client apps
|
||||
|
||||
## The easy way
|
||||
|
||||
[**Not working yet! Refer to 'Do it yourself'**]
|
||||
|
||||
For most client apps the default script will be enough:
|
||||
|
||||
```
|
||||
docker exec \
|
||||
-e APP_ID=<app name> \
|
||||
-e APP_SECRET=<app secret> \
|
||||
-e APP_CALLBACKS=<app callback url wiht https://...> \
|
||||
hydra /bin/sh -c "scripts/add_app.sh"
|
||||
```
|
||||
|
||||
## Do it yourself
|
||||
For example we will be creating a moodle app client. You need to adapt the vars to your app
|
||||
|
||||
```
|
||||
DOMAIN=(your domain root as set in main.conf)
|
||||
APP_ID=moodle
|
||||
APP_SECRET=Sup3rS3cr3t
|
||||
```
|
||||
```
|
||||
docker-compose exec hydra \
|
||||
hydra clients create \
|
||||
--endpoint http://hydra:4445/ \
|
||||
--id $APP_ID \
|
||||
--secret $APP_SECRET \
|
||||
--grant-types client_credentials,authorization_code,refresh_token \
|
||||
--token-endpoint-auth-m01d4f1c0-8a53-4df5-8a46-de670f42a4dfethod client_secret_post \
|
||||
--response-types code \
|
||||
--scope openid,offline,profile,email \
|
||||
--callbacks https://moodle.${DOMAIN}/auth/oidc/
|
||||
```
|
||||
|
||||
And then validate the app:
|
||||
```
|
||||
docker-compose exec hydra \
|
||||
hydra token client \
|
||||
--endpoint http://hydra:4444/ \
|
||||
--client-id $APP_ID \
|
||||
--client-secret $APP_SECRET
|
||||
```
|
||||
|
||||
## Example configuration for Moodle OpenID Connect
|
||||
|
||||
- authendpoint: https://hydra.${DOMAIN}/oauth2/auth
|
||||
- tokenendpoint: https://hydra.${DOMAIN}/oauth2/token
|
||||
- oidcresource: https://hydra.${DOMAIN}/userinfo
|
||||
- oidcscope: openid profile email
|
||||
- single_sign_off: [checked]
|
||||
- logouturi: https://login.${DOMAIN}/auth/logout
|
||||
|
||||
## Example configuration for Nextcloud Custom OpenID Connect
|
||||
- Prevent creating an account if the email address exists
|
||||
- Update user profile every login
|
||||
- Do not prune not available user groups on login
|
||||
- automatically create groups if they do not exist
|
||||
|
||||
- Authorize url: https://hydra.${DOMAIN}/oauth2/auth
|
||||
- Token url: https://hydra.${DOMAIN}/oauth2/token
|
||||
- User info URL (optional): https://hydra.${DOMAIN}/userinfo
|
||||
- Scope: openid profile offline email
|
||||
- Logout URL (optional): https://login.${DOMAIN}/auth/logout
|
||||
|
||||
### Be aware on nextcloud behind proxy
|
||||
Behind proxy we should force nextcloud-app to use https. From documentation it should be NEXTCLOUD_OVERWRITEPROTOCOL but it is not:
|
||||
- https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/reverse_proxy_configuration.html
|
||||
|
||||
Instead we used in docker/nextcloud/nextcloud.yml compose file the envvar NC_overwriteprotocol as stated in this thread:
|
||||
- https://github.com/nextcl01d4f1c0-8a53-4df5-8a46-de670f42a4dfoud/docker/pull/819
|
||||
|
||||
Extra information about this:
|
||||
Some users may get strange reply(Callback) url error from provider even if you pasted the right url, that's because your nextcloud
|
||||
server may generate http urls when you are actually using https. Please set 'overwriteprotocol' => 'https', in your config.php file.
|
||||
|
||||
<?php
|
||||
$CONFIG = array (
|
||||
'overwriteprotocol' => 'https',
|
||||
'memcache.local' => '\\OC\\Memcache\\APCu',
|
||||
'apps_paths' =>
|
||||
|
||||
## Nexcloud autoredirect for unauthorized users
|
||||
Set social_login_auto_redirect and social_login_http_client in config.php
|
||||
- https://apps.nextcloud.com/apps/sociallogin
|
||||
|
||||
### APPENDIX: Nextcloud Social Login plugin
|
||||
|
||||
|
||||
Config
|
||||
|
||||
You can use 'social_login_auto_redirect' => true setting in config.php for auto redirect unauthorized users to social login if only one
|
||||
provider is configured. If you want to temporary disable this function (e.g. for login as local admin), you can add noredir=1 query
|
||||
parameter in url for login page. Something like https://cloud.domain.com/login?noredir=1
|
||||
|
||||
To set timeout for http client, you can use
|
||||
|
||||
'social_login_http_client' => [
|
||||
'timeout' => 45,
|
||||
],
|
||||
|
||||
# IsardVDI office apps
|
||||
|
||||
Refer to https://gitlab.com/isard/isard-office repository for sample moodle, nextcloud, jitsi (and more) apps
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
## FreeIPA
|
||||
|
||||
ldapsearch -x -b "dc=domain,dc=org" -H ldap://ipa.domain.org
|
||||
ldapsearch -x -b "dc=domain,dc=org" -H ldap://ipa.domain.org -D "uid=admin,cn=users,cn=compat,dc=domain,dc=org" -W
|
||||
|
||||
|
||||
# KEYCLOACK
|
||||
|
||||
Here’s a list of OIDC endpoints that the Keycloak publishes. These URLs are useful if you are using a non-Keycloak client adapter to talk OIDC with the auth server. These are all relative URLs and the root of the URL being the HTTP(S) protocol, hostname, and usually path prefixed with /auth: i.e. https://localhost:8080/auth
|
||||
|
||||
/realms/{realm-name}/protocol/isard-sso-connect/token
|
||||
|
||||
This is the URL endpoint for obtaining a temporary code in the Authorization Code Flow or for obtaining tokens via the Implicit Flow, Direct Grants, or Client Grants.
|
||||
/realms/{realm-name}/protocol/isard-sso-connect/auth
|
||||
|
||||
This is the URL endpoint for the Authorization Code Flow to turn a temporary code into a token.
|
||||
/realms/{realm-name}/protocol/isard-sso-connect/logout
|
||||
|
||||
This is the URL endpoint for performing logouts.
|
||||
/realms/{realm-name}/protocol/isard-sso-connect/userinfo
|
||||
|
||||
This is the URL endpoint for the User Info service described in the OIDC specification.
|
||||
|
||||
In all of these replace {realm-name} with the name of the realm.
|
||||
|
||||
|
||||
http://login.santantoni.duckns.org/auth/realms/master/protocol/isard-sso-connect/logout
|
|
@ -0,0 +1,18 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
#git submodule init
|
||||
#git submodule update --recursive --remote
|
||||
|
||||
cp main.conf .env
|
||||
|
||||
## BUILD_ROOT_PATH env
|
||||
# This is a workarround for
|
||||
# https://github.com/docker/compose/issues/7873
|
||||
# See also BUILD_ROOT_PATH sed section at the end of file
|
||||
echo "BUILD_ROOT_PATH=$(pwd)" >> .env
|
||||
|
||||
. ./.env
|
||||
|
||||
cp .env docker-compose-parts/
|
||||
docker-compose -f docker-compose-parts/haproxy.yml -f docker-compose-parts/freeipa.yml -f docker-compose-parts/keycloak.yml -f docker-compose-parts/avatars.yml -f docker-compose-parts/postgresql.yml -f docker-compose-parts/network.yml config > docker-compose.yml
|
|
@ -0,0 +1,17 @@
|
|||
version: '3.7'
|
||||
|
||||
services:
|
||||
isard-sso-avatars:
|
||||
image: minio/minio
|
||||
container_name: isard-sso-avatars
|
||||
volumes:
|
||||
- ${DATA_FOLDER}/avatars:/data
|
||||
- ${SRC_FOLDER}/avatars:/root/.minio
|
||||
environment:
|
||||
- MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE
|
||||
- MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
||||
# depends_on:
|
||||
# - ${KEYCLOAK_DB_ADDR}
|
||||
command: "server /data"
|
||||
networks:
|
||||
- isard_net
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
version: '3.7'
|
||||
services:
|
||||
isard-sso-freeipa:
|
||||
container_name: isard-sso-freeipa
|
||||
image: freeipa/freeipa-server:centos-8
|
||||
restart: unless-stopped
|
||||
hostname: ipa.${DOMAIN}
|
||||
environment:
|
||||
- IPA_SERVER_HOSTNAME=ipa.${DOMAIN}
|
||||
tty: true
|
||||
stdin_open: true
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
volumes:
|
||||
- ${BUILD_ROOT_PATH}/scripts/freeipa:/scripts
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:ro
|
||||
- ${DATA_FOLDER}/freeipa:/data
|
||||
sysctls:
|
||||
- net.ipv6.conf.lo.disable_ipv6=0
|
||||
- net.ipv6.conf.all.disable_ipv6=0
|
||||
security_opt:
|
||||
- "seccomp:unconfined"
|
||||
command:
|
||||
- -U
|
||||
- --domain=${DOMAIN}
|
||||
- --realm=${DOMAIN}
|
||||
- --http-pin=${IPA_ADMIN_PWD}
|
||||
- --dirsrv-pin=${IPA_ADMIN_PWD}
|
||||
- --ds-password=${IPA_ADMIN_PWD}
|
||||
- --admin-password=${IPA_ADMIN_PWD}
|
||||
- --no-host-dns
|
||||
#- --no-dnssec-validation
|
||||
#- --setup-dns
|
||||
#- --auto-forwarders
|
||||
#- --allow-zone-overlap
|
||||
- --unattended
|
||||
#ports:
|
||||
#- "53:53/udp"
|
||||
#- "53:53"
|
||||
#- "80:80"
|
||||
#- "443:443"
|
||||
#- "389:389"
|
||||
#- "636:636"
|
||||
#- "88:88"
|
||||
#- "464:464"
|
||||
#- "88:88/udp"
|
||||
#- "464:464/udp"
|
||||
#- "123:123/udp"
|
||||
#- "7389:7389"
|
||||
#- "9443:9443"
|
||||
#- "9444:9444"
|
||||
#- "9445:9445"
|
||||
env_file:
|
||||
- .env
|
||||
networks:
|
||||
isard_net:
|
||||
aliases:
|
||||
- ${DOMAIN}
|
||||
- ipa.${DOMAIN}
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
version: '3.7'
|
||||
services:
|
||||
isard-sso-haproxy:
|
||||
build:
|
||||
context: ${BUILD_ROOT_PATH}/docker/haproxy
|
||||
dockerfile: Dockerfile
|
||||
target: production
|
||||
container_name: isard-sso-haproxy
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ${SRC_FOLDER}/haproxy/letsencrypt:/etc/letsencrypt:rw
|
||||
- ${SRC_FOLDER}/haproxy/certs:/certs:rw
|
||||
networks:
|
||||
- isard_net
|
||||
ports:
|
||||
- published: 80
|
||||
target: 80
|
||||
- published: 443
|
||||
target: 443
|
||||
env_file:
|
||||
- .env
|
|
@ -0,0 +1,33 @@
|
|||
version: '3.7'
|
||||
|
||||
services:
|
||||
isard-sso-keycloak:
|
||||
image: quay.io/keycloak/keycloak:latest
|
||||
container_name: isard-sso-keycloak
|
||||
#hostname: sso.${DOMAIN}
|
||||
volumes:
|
||||
- ${BUILD_ROOT_PATH}/init/keycloak/jsons:/opt/jboss/keycloak/imports
|
||||
- ${BUILD_ROOT_PATH}/init/keycloak/scripts/:/opt/jboss/startup-scripts/
|
||||
- ${BUILD_ROOT_PATH}/docker/keycloak/themes:/opt/jboss/keycloak/themes/
|
||||
- ${BUILD_ROOT_PATH}/docker/keycloak/extensions/keycloak-avatar-minio-extension/:/opt/custom/deployments
|
||||
#- ${BUILD_ROOT_PATH}/docker/keycloak/extensions/keycloak-avatar-minio-extension/:/opt/custom/deployments
|
||||
- ${BUILD_ROOT_PATH}/docker/keycloak/extensions/keycloak-avatar-minio-extension/avatar-minio-extension-bundle/target/avatar-minio-extension-bundle-1.0.1.0-SNAPSHOT.ear:/opt/jboss/keycloak/standalone/deployments/avatar-minio-extension-bundle-1.0.1.0-SNAPSHOT.ear
|
||||
#- /opt/jboss/keycloak/standalone/configuration/keycloak-add-user.json
|
||||
environment:
|
||||
#- KEYCLOAK_IMPORT=/opt/jboss/keycloak/imports/realm-export.json
|
||||
- DB_VENDOR=POSTGRES
|
||||
- DB_ADDR=${KEYCLOAK_DB_ADDR}
|
||||
- DB_DATABASE=${KEYCLOAK_DB_DATABASE}
|
||||
- DB_USER=${KEYCLOAK_DB_USER}
|
||||
- DB_SCHEMA=public
|
||||
- DB_PASSWORD=${KEYCLOAK_DB_PASSWORD}
|
||||
- KEYCLOAK_USER=${KEYCLOAK_USER}
|
||||
- KEYCLOAK_PASSWORD=${KEYCLOAK_PASSWORD}
|
||||
- PROXY_ADDRESS_FORWARDING=true
|
||||
- KEYCLOAK_FRONTEND_URL=https://sso.${DOMAIN}/auth/
|
||||
#- KEYCLOAK_LOGLEVEL=ALL
|
||||
#- Dkeycloak.profile.feature.upload_scripts=enabled
|
||||
depends_on:
|
||||
- ${KEYCLOAK_DB_ADDR}
|
||||
networks:
|
||||
- isard_net
|
|
@ -0,0 +1,41 @@
|
|||
version: '3.6'
|
||||
|
||||
services:
|
||||
keycloak:
|
||||
build:
|
||||
context: ${BUILD_ROOT_PATH}/docker/keycloak
|
||||
dockerfile: Dockerfile
|
||||
target: production
|
||||
container_name: keycloak
|
||||
hostname: sso.mydomain.com
|
||||
volumes:
|
||||
- ./imports:/opt/jboss/keycloak/imports
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:ro
|
||||
environment:
|
||||
KEYCLOAK_IMPORT: /opt/jboss/keycloak/imports/realm-export.json -Dkeycloak.profile.feature.upload_scripts=enabled
|
||||
DB_VENDOR: POSTGRES
|
||||
DB_ADDR: postgres
|
||||
DB_DATABASE: keycloak
|
||||
DB_USER: keycloak
|
||||
DB_SCHEMA: public
|
||||
DB_PASSWORD: k3ycl0ak
|
||||
KEYCLOAK_USER: admin
|
||||
KEYCLOAK_PASSWORD: k3ycl0ak
|
||||
#KEYCLOAK_LOGLEVEL: ALL
|
||||
PROXY_ADDRESS_FORWARDING: "true"
|
||||
KEYCLOAK_FRONTEND_URL: https://sso.mydomain.com/auth
|
||||
# Uncomment the line below if you want to specify JDBC parameters. The parameter below is just an example, and it shouldn't be used in production without knowledge. It is highly recommended that you read the PostgreSQL JDBC driver documentation in order to use it.
|
||||
#JDBC_PARAMS: "ssl=true"
|
||||
#ports:
|
||||
# - 8080:8080
|
||||
#cap-add:
|
||||
# - SYS_ADMIN
|
||||
privileged: true
|
||||
depends_on:
|
||||
- postgres
|
||||
networks:
|
||||
- isard_net
|
||||
|
||||
networks:
|
||||
isard_net:
|
||||
name: isard_net
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
version: '3.7'
|
||||
networks:
|
||||
isard_net:
|
||||
name: isard_net
|
|
@ -0,0 +1,13 @@
|
|||
version: "3.6"
|
||||
services:
|
||||
isard-sso-postgresql:
|
||||
image: postgres:alpine
|
||||
container_name: isard-sso-postgresql
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- ${DB_FOLDER}/postgres:/var/lib/postgresql/data
|
||||
- ${BUILD_ROOT_PATH}/init/databases:/docker-entrypoint-initdb.d
|
||||
networks:
|
||||
- isard_net
|
|
@ -0,0 +1,13 @@
|
|||
FROM haproxy:alpine as production
|
||||
RUN apk add openssl certbot py-pip
|
||||
RUN pip install certbot-plugin-gandi
|
||||
|
||||
COPY letsencrypt-hook-deploy-concatenante.sh /
|
||||
COPY letsencrypt.sh /usr/local/sbin/
|
||||
COPY letsencrypt-renew-cron.sh /etc/periodic/daily/letsencrypt-renew
|
||||
COPY auto-generate-certs.sh /usr/local/sbin/
|
||||
COPY docker-entrypoint.sh /usr/local/bin/
|
||||
RUN rm /docker-entrypoint.sh
|
||||
RUN ln -s /usr/local/bin/docker-entrypoint.sh /
|
||||
RUN chmod 775 docker-entrypoint.sh
|
||||
ADD haproxy.conf /usr/local/etc/haproxy/haproxy.cfg
|
|
@ -0,0 +1,31 @@
|
|||
cd /certs
|
||||
|
||||
# Self signed cert generic data
|
||||
C=CA
|
||||
L=Barcelona
|
||||
O=localdomain
|
||||
CN_CA=$O
|
||||
CN_HOST=*.$O
|
||||
OU=$O
|
||||
|
||||
echo '#### Creating 2048-bit RSA key:'
|
||||
openssl genrsa -out ca-key.pem 2048
|
||||
|
||||
echo '#### Using the key to create a self-signed certificate to your CA:'
|
||||
openssl req -new -x509 -days 9999 -key ca-key.pem -out ca-cert.pem -sha256 \
|
||||
-subj "/C=$C/L=$L/O=$O/CN=$CN_CA"
|
||||
|
||||
echo '#### Creating server certificate:'
|
||||
openssl genrsa -out server-key.pem 2048
|
||||
|
||||
echo '#### Creating a certificate signing request for the server:'
|
||||
openssl req -new -key server-key.pem -sha256 -out server-key.csr \
|
||||
-subj "/CN=$CN_HOST"
|
||||
|
||||
echo '#### Creating server certificate:'
|
||||
RND=$(( ( RANDOM % 1000 ) + 1 ))
|
||||
openssl x509 -req -days 9999 -in server-key.csr -CA ca-cert.pem -CAkey ca-key.pem \
|
||||
-set_serial $RND -sha256 -out server-cert.pem
|
||||
|
||||
echo '#### Concatenate certs for haprox'
|
||||
cat server-cert.pem server-key.pem > chain.pem
|
|
@ -0,0 +1,27 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Set debug path password
|
||||
PASSWD=$(python3 -c 'import os,crypt,getpass; print(crypt.crypt(os.environ["IPA_ADMIN_PWD"], crypt.mksalt(crypt.METHOD_SHA512)))')
|
||||
sed -i "/^ user admin password/c\ user admin password $PASSWD" /usr/local/etc/haproxy/haproxy.cfg
|
||||
|
||||
LETSENCRYPT_DOMAIN="$DOMAIN" letsencrypt.sh
|
||||
|
||||
if [ ! -e "/certs/chain.pem" ]; then
|
||||
auto-generate-certs.sh
|
||||
fi
|
||||
|
||||
# first arg is `-f` or `--some-option`
|
||||
if [ "${1#-}" != "$1" ]; then
|
||||
set -- haproxy "$@"
|
||||
fi
|
||||
|
||||
if [ "$1" = 'haproxy' ]; then
|
||||
shift # "haproxy"
|
||||
# if the user wants "haproxy", let's add a couple useful flags
|
||||
# -W -- "master-worker mode" (similar to the old "haproxy-systemd-wrapper"; allows for reload via "SIGUSR2")
|
||||
# -db -- disables background mode
|
||||
set -- haproxy -W -db "$@"
|
||||
fi
|
||||
|
||||
exec "$@"
|
|
@ -0,0 +1,23 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
prepare.sh
|
||||
|
||||
if [ ! -f /certs/chain.pem ]; then
|
||||
auto-generate-certs.sh
|
||||
fi
|
||||
|
||||
# first arg is `-f` or `--some-option`
|
||||
if [ "${1#-}" != "$1" ]; then
|
||||
set -- haproxy "$@"
|
||||
fi
|
||||
|
||||
if [ "$1" = 'haproxy' ]; then
|
||||
shift # "haproxy"
|
||||
# if the user wants "haproxy", let's add a couple useful flags
|
||||
# -W -- "master-worker mode" (similar to the old "haproxy-systemd-wrapper"; allows for reload via "SIGUSR2")
|
||||
# -db -- disables background mode
|
||||
set -- haproxy -W -db "$@"
|
||||
fi
|
||||
|
||||
exec "$@"
|
|
@ -0,0 +1,145 @@
|
|||
resolvers mydns
|
||||
nameserver dns1 127.0.0.11:53
|
||||
|
||||
global
|
||||
# debug
|
||||
daemon
|
||||
log 127.0.0.1 local0
|
||||
tune.ssl.default-dh-param 2048
|
||||
|
||||
defaults
|
||||
mode http
|
||||
timeout connect 25s
|
||||
timeout client 25s
|
||||
timeout client-fin 25s
|
||||
timeout server 25s
|
||||
timeout tunnel 7200s
|
||||
option http-server-close
|
||||
option httpclose
|
||||
log global
|
||||
option httplog
|
||||
backlog 4096
|
||||
maxconn 2000
|
||||
option tcpka
|
||||
|
||||
frontend website
|
||||
mode http
|
||||
bind :80
|
||||
redirect scheme https if !{ ssl_fc }
|
||||
# http-request set-header SSL_CLIENT_CERT %[ssl_c_der,base64]
|
||||
http-request del-header ssl_client_cert unless { ssl_fc_has_crt }
|
||||
http-request set-header ssl_client_cert -----BEGIN\ CERTIFICATE-----\ %[ssl_c_der,base64]\ -----END\ CERTIFICATE-----\ if { ssl_fc_has_crt }
|
||||
bind :443 ssl crt /certs/chain.pem
|
||||
|
||||
#cookie JSESSIONID prefix nocache
|
||||
#use_backend be_hydra if { path_beg /hydra }
|
||||
#use_backend be_hydra if { path_beg /oauth2 }
|
||||
|
||||
acl is_nextcloud hdr_beg(host) nextcloud.
|
||||
acl is_moodle hdr_beg(host) moodle.
|
||||
acl is_jitsi hdr_beg(host) jitsi.
|
||||
acl is_oof hdr_beg(host) oof.
|
||||
acl is_wp hdr_sub(host) .wp.
|
||||
acl is_wp hdr_beg(host) wp.
|
||||
acl is_pad hdr_beg(host) pad.
|
||||
acl is_sso hdr_beg(host) sso.
|
||||
acl is_ipa hdr_beg(host) ipa.
|
||||
|
||||
use_backend be_nextcloud if is_nextcloud
|
||||
use_backend be_moodle if is_moodle
|
||||
use_backend be_jitsi if is_jitsi
|
||||
use_backend be_oof if is_oof
|
||||
use_backend be_wp if is_wp
|
||||
use_backend be_etherpad if is_pad
|
||||
use_backend be_sso if is_sso
|
||||
use_backend be_ipa if is_ipa
|
||||
|
||||
# default_backend be_sso
|
||||
|
||||
backend be_ipa
|
||||
mode http
|
||||
acl existing-x-forwarded-host req.hdr(X-Forwarded-Host) -m found
|
||||
acl existing-x-forwarded-proto req.hdr(X-Forwarded-Proto) -m found
|
||||
http-request add-header X-Forwarded-Host %[req.hdr(Host)] unless existing-x-forwarded-host
|
||||
http-request add-header X-Forwarded-Proto https unless existing-x-forwarded-proto
|
||||
server freeipa isard-sso-freeipa:443 check port 443 ssl verify none inter 5s rise 2 fall 10 resolvers mydns init-addr none
|
||||
|
||||
backend be_sso
|
||||
mode http
|
||||
option httpclose
|
||||
#option http-server-close
|
||||
option forwardfor
|
||||
acl existing-x-forwarded-host req.hdr(X-Forwarded-Host) -m found
|
||||
acl existing-x-forwarded-proto req.hdr(X-Forwarded-Proto) -m found
|
||||
http-request add-header X-Forwarded-Host %[req.hdr(Host)] unless existing-x-forwarded-host
|
||||
http-request add-header X-Forwarded-Proto https unless existing-x-forwarded-proto
|
||||
server keycloak isard-sso-keycloak:8080 check port 8080 inter 5s rise 2 fall 10 resolvers mydns init-addr none
|
||||
|
||||
## APPS
|
||||
backend be_moodle
|
||||
mode http
|
||||
acl existing-x-forwarded-host req.hdr(X-Forwarded-Host) -m found
|
||||
acl existing-x-forwarded-proto req.hdr(X-Forwarded-Proto) -m found
|
||||
http-request add-header X-Forwarded-Host %[req.hdr(Host)] unless existing-x-forwarded-host
|
||||
http-request add-header X-Forwarded-Proto https unless existing-x-forwarded-proto
|
||||
server moodle isard-apps-moodle:8080 check port 8080 inter 5s rise 2 fall 10 resolvers mydns init-addr none
|
||||
|
||||
backend be_nextcloud
|
||||
mode http
|
||||
acl existing-x-forwarded-host req.hdr(X-Forwarded-Host) -m found
|
||||
acl existing-x-forwarded-proto req.hdr(X-Forwarded-Proto) -m found
|
||||
http-request add-header X-Forwarded-Host %[req.hdr(Host)] unless existing-x-forwarded-host
|
||||
http-request add-header X-Forwarded-Proto https unless existing-x-forwarded-proto
|
||||
server nextcloud isard-apps-nextcloud-nginx:80 check port 80 inter 5s rise 2 fall 10 resolvers mydns init-addr none
|
||||
|
||||
backend be_etherpad
|
||||
mode http
|
||||
acl existing-x-forwarded-host req.hdr(X-Forwarded-Host) -m found
|
||||
acl existing-x-forwarded-proto req.hdr(X-Forwarded-Proto) -m found
|
||||
http-request add-header X-Forwarded-Host %[req.hdr(Host)] unless existing-x-forwarded-host
|
||||
http-request add-header X-Forwarded-Proto https unless existing-x-forwarded-proto
|
||||
server etherpad isard-apps-etherpad:9001 check port 9001 inter 5s rise 2 fall 10 resolvers mydns init-addr none
|
||||
|
||||
backend be_jitsi
|
||||
mode http
|
||||
acl existing-x-forwarded-host req.hdr(X-Forwarded-Host) -m found
|
||||
acl existing-x-forwarded-proto req.hdr(X-Forwarded-Proto) -m found
|
||||
http-request add-header X-Forwarded-Host %[req.hdr(Host)] unless existing-x-forwarded-host
|
||||
http-request add-header X-Forwarded-Proto https unless existing-x-forwarded-proto
|
||||
server jitsi isard-apps-jitsi:80 check port 80 inter 5s rise 2 fall 10 resolvers mydns init-addr none
|
||||
|
||||
backend be_oof
|
||||
mode http
|
||||
acl existing-x-forwarded-host req.hdr(X-Forwarded-Host) -m found
|
||||
acl existing-x-forwarded-proto req.hdr(X-Forwarded-Proto) -m found
|
||||
http-request add-header X-Forwarded-Host %[req.hdr(Host)] unless existing-x-forwarded-host
|
||||
http-request add-header X-Forwarded-Proto https unless existing-x-forwarded-proto
|
||||
server onlyoffice isard-apps-onlyoffice:80 check port 80 inter 5s rise 2 fall 10 resolvers mydns init-addr none
|
||||
|
||||
backend be_wp
|
||||
mode http
|
||||
acl existing-x-forwarded-host req.hdr(X-Forwarded-Host) -m found
|
||||
acl existing-x-forwarded-proto req.hdr(X-Forwarded-Proto) -m found
|
||||
http-request add-header X-Forwarded-Host %[req.hdr(Host)] unless existing-x-forwarded-host
|
||||
http-request add-header X-Forwarded-Proto https unless existing-x-forwarded-proto
|
||||
server wp isard-apps-wordpress:80 check port 80 inter 5s rise 2 fall 10 resolvers mydns init-addr none
|
||||
|
||||
|
||||
listen stats
|
||||
bind 0.0.0.0:8888
|
||||
mode http
|
||||
stats enable
|
||||
option httplog
|
||||
stats show-legends
|
||||
stats uri /haproxy
|
||||
stats realm Haproxy\ Statistics
|
||||
stats refresh 5s
|
||||
#stats auth staging:pep1n1ll0
|
||||
#acl authorized http_auth(AuthUsers)
|
||||
#stats http-request auth unless authorized
|
||||
timeout connect 5000ms
|
||||
timeout client 50000ms
|
||||
timeout server 50000ms
|
||||
|
||||
userlist AuthUsers
|
||||
user admin password $6$grgQMVfwI0XSGAQl$2usaQC9LVXXXYHtSkGUf74CIGsiH8fi/K.V6DuKSq0twPkmFGP2vL/b//Ulp2I4xBEZ3eYDhUbwBPK8jpmsbo.
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
cat $RENEWED_LINEAGE/fullchain.pem $RENEWED_LINEAGE/privkey.pem > /certs/chain.pem
|
||||
|
||||
kill -SIGUSR2 1
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
certbot renew --cert-name $LETSENCRYPT_DOMAIN
|
|
@ -0,0 +1,28 @@
|
|||
#!/bin/sh
|
||||
if [ -f /letsencrypt-hook-deploy-concatenante.sh ]
|
||||
then
|
||||
mkdir -p /etc/letsencrypt/renewal-hooks/deploy/
|
||||
mv /letsencrypt-hook-deploy-concatenante.sh /etc/letsencrypt/renewal-hooks/deploy/concatenate.sh
|
||||
fi
|
||||
|
||||
|
||||
if [ -n "$LETSENCRYPT_DOMAIN" -a -n "$LETSENCRYPT_EMAIL" ]
|
||||
then
|
||||
LETSENCRYPT_DOMAIN="$LETSENCRYPT_DOMAIN" crond
|
||||
if [ -n "$GANDI_KEY" ]
|
||||
then
|
||||
touch /gandi.ini
|
||||
chmod 600 /gandi.ini
|
||||
echo "dns_gandi_api_key=$GANDI_KEY" > /gandi.ini
|
||||
GANDI_OPTIONS="--authenticator dns-gandi --dns-gandi-credentials /gandi.ini"
|
||||
else
|
||||
GANDI_OPTIONS=""
|
||||
fi
|
||||
if [ ! -f /certs/chain.pem ]
|
||||
then
|
||||
if certbot certonly $GANDI_OPTIONS -d "$LETSENCRYPT_DOMAIN" -d "*.$LETSENCRYPT_DOMAIN" -m "$LETSENCRYPT_EMAIL" -n --agree-tos
|
||||
then
|
||||
RENEWED_LINEAGE="/etc/letsencrypt/live/$LETSENCRYPT_DOMAIN" /etc/letsencrypt/renewal-hooks/deploy/concatenate.sh
|
||||
fi
|
||||
fi
|
||||
fi
|
|
@ -0,0 +1,6 @@
|
|||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache py3-pip
|
||||
RUN pip3 install python-keycloak
|
||||
ADD kcli.py /
|
||||
COPY dumps /
|
|
@ -0,0 +1,25 @@
|
|||
import json, sys
|
||||
from pprint import pprint
|
||||
|
||||
from keycloak import KeycloakAdmin
|
||||
|
||||
def keycloak_connect():
|
||||
keycloak_admin = KeycloakAdmin(server_url="http://isard-sso-keycloak:8080/auth/",
|
||||
username='admin',
|
||||
password='keycloakkeycloak',
|
||||
realm_name="master",
|
||||
user_realm_name="only_if_other_realm_than_master",
|
||||
client_secret_key="client-secret",
|
||||
verify=True)
|
||||
|
||||
def keycloak_dumps():
|
||||
print('Dumping keycloak config...')
|
||||
|
||||
def Keycloak_imports():
|
||||
with open('saml_client.json') as json_file:
|
||||
data = json.load(json_file)
|
||||
|
||||
if __name__ == "__main__":
|
||||
keycloak_connect()
|
||||
if sys.argv[1]=='dump':
|
||||
keycloak_dumps()
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"id" : "5254c35b-18b2-405d-ad88-80e870b14554",
|
||||
"clientId" : "https://moodle.isardvdi.site/auth/saml2/sp/metadata.php",
|
||||
"surrogateAuthRequired" : false,
|
||||
"enabled" : true,
|
||||
"alwaysDisplayInConsole" : false,
|
||||
"clientAuthenticatorType" : "client-secret",
|
||||
"redirectUris" : [ "https://moodle.isardvdi.site/auth/saml2/sp/saml2-acs.php/moodle.isardvdi.site" ],
|
||||
"webOrigins" : [ "https://moodle.isardvdi.site" ],
|
||||
"notBefore" : 0,
|
||||
"bearerOnly" : false,
|
||||
"consentRequired" : false,
|
||||
"standardFlowEnabled" : true,
|
||||
"implicitFlowEnabled" : false,
|
||||
"directAccessGrantsEnabled" : false,
|
||||
"serviceAccountsEnabled" : false,
|
||||
"publicClient" : false,
|
||||
"frontchannelLogout" : true,
|
||||
"protocol" : "saml",
|
||||
"attributes" : {
|
||||
"saml.force.post.binding" : "true",
|
||||
"saml.encrypt" : "true",
|
||||
"saml_assertion_consumer_url_post" : "https://moodle.isardvdi.site/auth/saml2/sp/saml2-acs.php/moodle.isardvdi.site",
|
||||
"saml.server.signature" : "true",
|
||||
"saml.server.signature.keyinfo.ext" : "false",
|
||||
"saml.signing.certificate" : "MIID4DCCAsigAwIBAgIBADANBgkqhkiG9w0BAQsFADCBiDEPMA0GA1UEAwwGbW9vZGxlMQswCQYDVQQGEwJBVTEfMB0GCSqGSIb3DQEJARYQdXNlckBleGFtcGxlLmNvbTEUMBIGA1UEBwwLbW9vZGxldmlsbGUxDzANBgNVBAoMBm1vb2RsZTEPMA0GA1UECAwGbW9vZGxlMQ8wDQYDVQQLDAZtb29kbGUwHhcNMjEwMzE1MTQwNTQ4WhcNMzEwMzEzMTQwNTQ4WjCBiDEPMA0GA1UEAwwGbW9vZGxlMQswCQYDVQQGEwJBVTEfMB0GCSqGSIb3DQEJARYQdXNlckBleGFtcGxlLmNvbTEUMBIGA1UEBwwLbW9vZGxldmlsbGUxDzANBgNVBAoMBm1vb2RsZTEPMA0GA1UECAwGbW9vZGxlMQ8wDQYDVQQLDAZtb29kbGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCV/f0IHhJhggl1HM4v2CYYkSaZvNeanNZtelGFWHKZnRUATRrVZVMNuEkyNCnHWifOvLdnkKy9kspUUOzhoTSVqq/9Piuru43kOuYL/kkrDyJm2RmMG1Oi2/Eldqx9PBpoUPW9/ORwSz7noRBLQcfkwxtt6Xa3Ye0be2fUfSy9IjMAU/to4tW60elZU1j0zllZHuk8ACCUA7M4BqFeVXZTrUDpu/rtzzboH8ti6/QwPbqsp9NsB1IFVFQM9QqtTFq3T2226ZGpgqeAJd8urx3ghJpGP9v6+O3g/LX99a/wqESYhU0yJ8Yyp4IL/P1aJjjB5bHqLT1pY/lTt397Zz/pAgMBAAGjUzBRMB0GA1UdDgQWBBTFQrz8CcnAmsA9fXJqewO2JcXMtjAfBgNVHSMEGDAWgBTFQrz8CcnAmsA9fXJqewO2JcXMtjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCKnn0TTvaVFfcb3NKUJQpCQQuPTLekXpVcQYK3alnfkNQ496yvtQ0gxet4gae4cGPCD8VkUgsSuIn7VeHwPVTeWHPcMH5nn4zI5+77jyZVaoDao8e7ZWo0qP5xhK/qvTARAWO3QGxfD480OT92wNX5IUuYb+XJ5yAAxXzMPJFt6tkkHfkLDaqnGgFYmtKo08fmbdX5232aAaN0gQOYdaybRt1Q3rOnlBmUoDatjertfutDfi312MS3S4e5mOaMOIaM2CupDnICRoXiv6/lBQu6N7c3ABzphzFlO4pXEqJhq2dOgfhIYD/HVhS77dGGj6n04UWN2OhN31mwtJ9D6aSk",
|
||||
"saml_single_logout_service_url_redirect" : "https://moodle.isardvdi.site/auth/saml2/sp/saml2-logout.php/moodle.isardvdi.site",
|
||||
"saml.signature.algorithm" : "RSA_SHA256",
|
||||
"saml_force_name_id_format" : "false",
|
||||
"saml.client.signature" : "true",
|
||||
"saml.encryption.certificate" : "MIID4DCCAsigAwIBAgIBADANBgkqhkiG9w0BAQsFADCBiDEPMA0GA1UEAwwGbW9vZGxlMQswCQYDVQQGEwJBVTEfMB0GCSqGSIb3DQEJARYQdXNlckBleGFtcGxlLmNvbTEUMBIGA1UEBwwLbW9vZGxldmlsbGUxDzANBgNVBAoMBm1vb2RsZTEPMA0GA1UECAwGbW9vZGxlMQ8wDQYDVQQLDAZtb29kbGUwHhcNMjEwMzE1MTQwNTQ4WhcNMzEwMzEzMTQwNTQ4WjCBiDEPMA0GA1UEAwwGbW9vZGxlMQswCQYDVQQGEwJBVTEfMB0GCSqGSIb3DQEJARYQdXNlckBleGFtcGxlLmNvbTEUMBIGA1UEBwwLbW9vZGxldmlsbGUxDzANBgNVBAoMBm1vb2RsZTEPMA0GA1UECAwGbW9vZGxlMQ8wDQYDVQQLDAZtb29kbGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCV/f0IHhJhggl1HM4v2CYYkSaZvNeanNZtelGFWHKZnRUATRrVZVMNuEkyNCnHWifOvLdnkKy9kspUUOzhoTSVqq/9Piuru43kOuYL/kkrDyJm2RmMG1Oi2/Eldqx9PBpoUPW9/ORwSz7noRBLQcfkwxtt6Xa3Ye0be2fUfSy9IjMAU/to4tW60elZU1j0zllZHuk8ACCUA7M4BqFeVXZTrUDpu/rtzzboH8ti6/QwPbqsp9NsB1IFVFQM9QqtTFq3T2226ZGpgqeAJd8urx3ghJpGP9v6+O3g/LX99a/wqESYhU0yJ8Yyp4IL/P1aJjjB5bHqLT1pY/lTt397Zz/pAgMBAAGjUzBRMB0GA1UdDgQWBBTFQrz8CcnAmsA9fXJqewO2JcXMtjAfBgNVHSMEGDAWgBTFQrz8CcnAmsA9fXJqewO2JcXMtjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCKnn0TTvaVFfcb3NKUJQpCQQuPTLekXpVcQYK3alnfkNQ496yvtQ0gxet4gae4cGPCD8VkUgsSuIn7VeHwPVTeWHPcMH5nn4zI5+77jyZVaoDao8e7ZWo0qP5xhK/qvTARAWO3QGxfD480OT92wNX5IUuYb+XJ5yAAxXzMPJFt6tkkHfkLDaqnGgFYmtKo08fmbdX5232aAaN0gQOYdaybRt1Q3rOnlBmUoDatjertfutDfi312MS3S4e5mOaMOIaM2CupDnICRoXiv6/lBQu6N7c3ABzphzFlO4pXEqJhq2dOgfhIYD/HVhS77dGGj6n04UWN2OhN31mwtJ9D6aSk",
|
||||
"saml.authnstatement" : "true",
|
||||
"saml_name_id_format" : "username",
|
||||
"saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#"
|
||||
},
|
||||
"authenticationFlowBindingOverrides" : { },
|
||||
"fullScopeAllowed" : true,
|
||||
"nodeReRegistrationTimeout" : -1,
|
||||
"defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ],
|
||||
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ],
|
||||
"access" : {
|
||||
"view" : true,
|
||||
"configure" : true,
|
||||
"manage" : true
|
||||
}
|
||||
},
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,95 @@
|
|||
{
|
||||
"id" : "0457e842-172b-42bf-8569-814625e7b019",
|
||||
"clientId" : "https://moodle.santantoni.duckdns.org/auth/saml2/sp/metadata.php",
|
||||
"surrogateAuthRequired" : false,
|
||||
"enabled" : true,
|
||||
"alwaysDisplayInConsole" : false,
|
||||
"clientAuthenticatorType" : "client-secret",
|
||||
"redirectUris" : [ "https://moodle.santantoni.duckdns.org/auth/saml2/sp/saml2-acs.php/moodle.santantoni.duckdns.org" ],
|
||||
"webOrigins" : [ "https://moodle.santantoni.duckdns.org" ],
|
||||
"notBefore" : 0,
|
||||
"bearerOnly" : false,
|
||||
"consentRequired" : false,
|
||||
"standardFlowEnabled" : true,
|
||||
"implicitFlowEnabled" : false,
|
||||
"directAccessGrantsEnabled" : false,
|
||||
"serviceAccountsEnabled" : false,
|
||||
"publicClient" : false,
|
||||
"frontchannelLogout" : true,
|
||||
"protocol" : "saml",
|
||||
"attributes" : {
|
||||
"saml.force.post.binding" : "true",
|
||||
"saml.encrypt" : "true",
|
||||
"saml_assertion_consumer_url_post" : "https://moodle.santantoni.duckdns.org/auth/saml2/sp/saml2-acs.php/moodle.santantoni.duckdns.org",
|
||||
"saml.server.signature" : "true",
|
||||
"saml.server.signature.keyinfo.ext" : "false",
|
||||
"saml.signing.certificate" : "MIIECjCCAvKgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBnTEPMA0GA1UEAwwGbW9vZGxlMQswCQYDVQQGEwJBVTEUMBIGA1UEBwwLbW9vZGxldmlsbGUxNDAyBgkqhkiG9w0BCQEWJW5vcmVwbHlAbW9vZGxlLnNhbnRhbnRvbmkuZHVja2Rucy5vcmcxDzANBgNVBAoMBm1vb2RsZTEPMA0GA1UECAwGbW9vZGxlMQ8wDQYDVQQLDAZtb29kbGUwHhcNMjEwNDE4MDkxODE5WhcNMzEwNDE2MDkxODE5WjCBnTEPMA0GA1UEAwwGbW9vZGxlMQswCQYDVQQGEwJBVTEUMBIGA1UEBwwLbW9vZGxldmlsbGUxNDAyBgkqhkiG9w0BCQEWJW5vcmVwbHlAbW9vZGxlLnNhbnRhbnRvbmkuZHVja2Rucy5vcmcxDzANBgNVBAoMBm1vb2RsZTEPMA0GA1UECAwGbW9vZGxlMQ8wDQYDVQQLDAZtb29kbGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHwSt88KWeyVVOIn452hwLgWScvClh0B+U1k5SaLsmvodbGFwhYpedAWjVsdJvCIopmB0XFmffLl5OJMGA8arUxmiVMKlaX3gNi3B6GIpcEADxA+KHJz5LnOPH6zIFUNRLtXIdQo+QTpCkxkeRh3VGE3kiEKtMlPhNCJR3EADTgd5NgDJ8dcyPmk76GoynnBDObICOQtbydlsBVAlxa/WwVKa6SLTsGrMZnDBDF4PATBvzqOJTGEqc7S5lNs95je7vlkEukSj2xGfo6Dk4OYa0dgV5vR7fMwH71JppuwG0pqY5//YFaQ5hz9eCUJwDnaCWi0KPxMAxCh0XakXrTvAFAgMBAAGjUzBRMB0GA1UdDgQWBBRUksQbGQ74ntYNzRflVwMjN85Q+jAfBgNVHSMEGDAWgBRUksQbGQ74ntYNzRflVwMjN85Q+jAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBkJJ3QGTi0hAM34XSnu9C+JbFgwpY6T2FkhSOgRY3NwNnR/IP03uxL1IwxwarN7KnkUwv2SYztpIa/MSnPeQ/c8ZlSh7EOLog1fNu1Rn4H4O5gw2GNixsz3TtR8dQkmXHdiGFmGL3XukLHj3PXpJciLoh0+i3xPyV2xNcYURxEnMx3fQ6f+0Me1JBMt3BgWbv9UZCKX6VDnpSTH/qcgRHKdenHnbjZXP22A8HkLDpQO7e5sBv+RNs8UwQiNI3NxZyjLLyNJdzAp0Z2UfC/jYRooQiehOdkqx2BS00l1dQfcQGvEBfgRWWkMdn8wE62blTofcQqfz3K30DEijLeHDks",
|
||||
"saml_single_logout_service_url_redirect" : "https://moodle.santantoni.duckdns.org/auth/saml2/sp/saml2-logout.php/moodle.santantoni.duckdns.org",
|
||||
"saml.signature.algorithm" : "RSA_SHA256",
|
||||
"saml_force_name_id_format" : "false",
|
||||
"saml.client.signature" : "true",
|
||||
"saml.encryption.certificate" : "MIIECjCCAvKgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBnTEPMA0GA1UEAwwGbW9vZGxlMQswCQYDVQQGEwJBVTEUMBIGA1UEBwwLbW9vZGxldmlsbGUxNDAyBgkqhkiG9w0BCQEWJW5vcmVwbHlAbW9vZGxlLnNhbnRhbnRvbmkuZHVja2Rucy5vcmcxDzANBgNVBAoMBm1vb2RsZTEPMA0GA1UECAwGbW9vZGxlMQ8wDQYDVQQLDAZtb29kbGUwHhcNMjEwNDE4MDkxODE5WhcNMzEwNDE2MDkxODE5WjCBnTEPMA0GA1UEAwwGbW9vZGxlMQswCQYDVQQGEwJBVTEUMBIGA1UEBwwLbW9vZGxldmlsbGUxNDAyBgkqhkiG9w0BCQEWJW5vcmVwbHlAbW9vZGxlLnNhbnRhbnRvbmkuZHVja2Rucy5vcmcxDzANBgNVBAoMBm1vb2RsZTEPMA0GA1UECAwGbW9vZGxlMQ8wDQYDVQQLDAZtb29kbGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHwSt88KWeyVVOIn452hwLgWScvClh0B+U1k5SaLsmvodbGFwhYpedAWjVsdJvCIopmB0XFmffLl5OJMGA8arUxmiVMKlaX3gNi3B6GIpcEADxA+KHJz5LnOPH6zIFUNRLtXIdQo+QTpCkxkeRh3VGE3kiEKtMlPhNCJR3EADTgd5NgDJ8dcyPmk76GoynnBDObICOQtbydlsBVAlxa/WwVKa6SLTsGrMZnDBDF4PATBvzqOJTGEqc7S5lNs95je7vlkEukSj2xGfo6Dk4OYa0dgV5vR7fMwH71JppuwG0pqY5//YFaQ5hz9eCUJwDnaCWi0KPxMAxCh0XakXrTvAFAgMBAAGjUzBRMB0GA1UdDgQWBBRUksQbGQ74ntYNzRflVwMjN85Q+jAfBgNVHSMEGDAWgBRUksQbGQ74ntYNzRflVwMjN85Q+jAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBkJJ3QGTi0hAM34XSnu9C+JbFgwpY6T2FkhSOgRY3NwNnR/IP03uxL1IwxwarN7KnkUwv2SYztpIa/MSnPeQ/c8ZlSh7EOLog1fNu1Rn4H4O5gw2GNixsz3TtR8dQkmXHdiGFmGL3XukLHj3PXpJciLoh0+i3xPyV2xNcYURxEnMx3fQ6f+0Me1JBMt3BgWbv9UZCKX6VDnpSTH/qcgRHKdenHnbjZXP22A8HkLDpQO7e5sBv+RNs8UwQiNI3NxZyjLLyNJdzAp0Z2UfC/jYRooQiehOdkqx2BS00l1dQfcQGvEBfgRWWkMdn8wE62blTofcQqfz3K30DEijLeHDks",
|
||||
"saml.authnstatement" : "true",
|
||||
"saml_name_id_format" : "username",
|
||||
"saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#"
|
||||
},
|
||||
"authenticationFlowBindingOverrides" : { },
|
||||
"fullScopeAllowed" : true,
|
||||
"nodeReRegistrationTimeout" : -1,
|
||||
"protocolMappers" : [ {
|
||||
"id" : "6e05e32c-436a-4b35-a376-801058b757ac",
|
||||
"name" : "X500 email",
|
||||
"protocol" : "saml",
|
||||
"protocolMapper" : "saml-user-property-mapper",
|
||||
"consentRequired" : false,
|
||||
"config" : {
|
||||
"attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
|
||||
"user.attribute" : "email",
|
||||
"friendly.name" : "email",
|
||||
"attribute.name" : "urn:oid:1.2.840.113549.1.9.1"
|
||||
}
|
||||
}, {
|
||||
"id" : "0d5e1994-1de1-49cd-bad5-f48c31926019",
|
||||
"name" : "X500 givenName",
|
||||
"protocol" : "saml",
|
||||
"protocolMapper" : "saml-user-property-mapper",
|
||||
"consentRequired" : false,
|
||||
"config" : {
|
||||
"attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
|
||||
"user.attribute" : "firstName",
|
||||
"friendly.name" : "givenName",
|
||||
"attribute.name" : "urn:oid:2.5.4.42"
|
||||
}
|
||||
}, {
|
||||
"id" : "8807929f-20da-4185-b37a-fa414bbe7a35",
|
||||
"name" : "username",
|
||||
"protocol" : "saml",
|
||||
"protocolMapper" : "saml-user-property-mapper",
|
||||
"consentRequired" : false,
|
||||
"config" : {
|
||||
"attribute.nameformat" : "Basic",
|
||||
"user.attribute" : "username",
|
||||
"friendly.name" : "username",
|
||||
"attribute.name" : "username"
|
||||
}
|
||||
}, {
|
||||
"id" : "04ad9cbb-f7cf-4759-8993-9b9ce5075ebc",
|
||||
"name" : "X500 surname",
|
||||
"protocol" : "saml",
|
||||
"protocolMapper" : "saml-user-property-mapper",
|
||||
"consentRequired" : false,
|
||||
"config" : {
|
||||
"attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
|
||||
"user.attribute" : "lastName",
|
||||
"friendly.name" : "surname",
|
||||
"attribute.name" : "urn:oid:2.5.4.4"
|
||||
}
|
||||
} ],
|
||||
"defaultClientScopes" : [ "web-origins", "role_list", "profile", "roles", "email" ],
|
||||
"optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ],
|
||||
"access" : {
|
||||
"view" : true,
|
||||
"configure" : true,
|
||||
"manage" : true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
FROM centos:centos8 as production
|
||||
# https://mtembo.com/technology/installing-keycloak/
|
||||
#RUN dnf update -y
|
||||
RUN dnf install -y java-1.8.0-openjdk-devel wget curl zip
|
||||
RUN mkdir -p /opt/keycloak
|
||||
RUN wget https://github.com/keycloak/keycloak/releases/download/12.0.4/keycloak-12.0.4.zip -P /opt/keycloak
|
||||
WORKDIR /opt/keycloak
|
||||
RUN unzip keycloak-12.0.4.zip -d /opt/keycloak
|
||||
WORKDIR /opt/keycloak/keycloak-12.0.4
|
||||
|
||||
### Proxy environment. This should be done in keycloak.cli but don't know how
|
||||
RUN sed -i 's/<http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true"\/>/<http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true" proxy-address-forwarding="true"\/>/g' standalone/configuration/standalone.xml
|
||||
# postgres
|
||||
# Download PostgreSQL database drivers
|
||||
#RUN mkdir -p /opt/drivers/jdbc
|
||||
#RUN wget https://jdbc.postgresql.org/download/postgresql-42.2.16.jar -P /opt/drivers/jdbc
|
||||
#RUN /opt/keycloak/keycloak-12.0.4/bin/jboss-cli.sh -c --commands="module add --name=org.postgresql --dependencies=javax.api,javax.transaction.api --resources=/opt/drivers/jdbc/postgresql-42.2.16.jar"
|
||||
#RUN /subsystem=datasources/jdbc-driver=postgresql:add(driver-name=postgresql,driver-module-name=org.postgresql,driver-class-name=org.postgresql.Driver)
|
||||
#RUN /subsystem=datasources/data-source=KeycloakDS:remove
|
||||
#RUN /subsystem=datasources/data-source=KeycloakDS:add(driver-name=postgresql,enabled=true,use-java-context=true,connection-url="jdbc:postgresql://host:port/database",jndi-name="java:/jboss/datasources/KeycloakDS",user-name=keycloak,password="PASSWORD",max-pool-size=20)
|
||||
|
||||
#RUN standalone.sh -b 0.0.0.0 -Djboss.http.port=9080 -Dkeycloak.import=/keycloak-work/freeipa-realm.json
|
||||
RUN dnf -y install systemd; dnf clean all; \
|
||||
(cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == systemd-tmpfiles-setup.service ] || rm -f $i; done); \
|
||||
rm -f /lib/systemd/system/multi-user.target.wants/*;\
|
||||
rm -f /etc/systemd/system/*.wants/*;\
|
||||
rm -f /lib/systemd/system/local-fs.target.wants/*; \
|
||||
rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
|
||||
rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
|
||||
rm -f /lib/systemd/system/basic.target.wants/*;\
|
||||
rm -f /lib/systemd/system/anaconda.target.wants/*;
|
||||
|
||||
RUN dnf -y install freeipa-client freeipa-admintools jna sssd-dbus
|
||||
RUN wget https://download.copr.fedorainfracloud.org/results/giesen/libunix-dbus-java/epel-8-x86_64/01134406-libunix-dbus-java/libunix-dbus-java-0.8.0-2.el8.x86_64.rpm
|
||||
RUN dnf install -y libunix-dbus-java-0.8.0-2.el8.x86_64.rpm
|
||||
|
||||
ADD docker-entrypoint.sh /
|
||||
RUN chmod 700 /docker-entrypoint.sh
|
||||
CMD ["/docker-entrypoint.sh"]
|
||||
#CMD ["/opt/keycloak/keycloak-12.0.4/bin/standalone.sh","-b","0.0.0.0"]
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
FROM centos/systemd as production
|
||||
# https://mtembo.com/technology/installing-keycloak/
|
||||
#RUN yum update -y
|
||||
RUN yum install -y java-1.8.0-openjdk-devel wget curl zip unzip
|
||||
RUN mkdir -p /opt/keycloak
|
||||
RUN wget https://github.com/keycloak/keycloak/releases/download/12.0.4/keycloak-12.0.4.zip -P /opt/keycloak
|
||||
WORKDIR /opt/keycloak
|
||||
RUN unzip keycloak-12.0.4.zip -d /opt/keycloak
|
||||
WORKDIR /opt/keycloak/keycloak-12.0.4
|
||||
|
||||
### Proxy environment. This should be done in keycloak.cli but don't know how
|
||||
RUN sed -i 's/<http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true"\/>/<http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true" proxy-address-forwarding="true"\/>/g' standalone/configuration/standalone.xml
|
||||
# postgres
|
||||
# Download PostgreSQL database drivers
|
||||
#RUN mkdir -p /opt/drivers/jdbc
|
||||
#RUN wget https://jdbc.postgresql.org/download/postgresql-42.2.16.jar -P /opt/drivers/jdbc
|
||||
#RUN /opt/keycloak/keycloak-12.0.4/bin/jboss-cli.sh -c --commands="module add --name=org.postgresql --dependencies=javax.api,javax.transaction.api --resources=/opt/drivers/jdbc/postgresql-42.2.16.jar"
|
||||
#RUN /subsystem=datasources/jdbc-driver=postgresql:add(driver-name=postgresql,driver-module-name=org.postgresql,driver-class-name=org.postgresql.Driver)
|
||||
#RUN /subsystem=datasources/data-source=KeycloakDS:remove
|
||||
#RUN /subsystem=datasources/data-source=KeycloakDS:add(driver-name=postgresql,enabled=true,use-java-context=true,connection-url="jdbc:postgresql://host:port/database",jndi-name="java:/jboss/datasources/KeycloakDS",user-name=keycloak,password="PASSWORD",max-pool-size=20)
|
||||
|
||||
#RUN standalone.sh -b 0.0.0.0 -Djboss.http.port=9080 -Dkeycloak.import=/keycloak-work/freeipa-realm.json
|
||||
RUN yum -y install systemd; yum clean all; \
|
||||
(cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == systemd-tmpfiles-setup.service ] || rm -f $i; done); \
|
||||
rm -f /lib/systemd/system/multi-user.target.wants/*;\
|
||||
rm -f /etc/systemd/system/*.wants/*;\
|
||||
rm -f /lib/systemd/system/local-fs.target.wants/*; \
|
||||
rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
|
||||
rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
|
||||
rm -f /lib/systemd/system/basic.target.wants/*;\
|
||||
rm -f /lib/systemd/system/anaconda.target.wants/*;
|
||||
|
||||
RUN yum -y install freeipa-client freeipa-admintools jna sssd-dbus
|
||||
RUN wget https://download.copr.fedorainfracloud.org/results/giesen/libunix-dbus-java/epel-8-x86_64/01134406-libunix-dbus-java/libunix-dbus-java-0.8.0-2.el8.x86_64.rpm
|
||||
RUN yum install -y libunix-dbus-java-0.8.0-2.el8.x86_64.rpm
|
||||
|
||||
ADD docker-entrypoint.sh /
|
||||
RUN chmod 700 /docker-entrypoint.sh
|
||||
CMD ["/docker-entrypoint.sh"]
|
||||
#CMD ["/opt/keycloak/keycloak-12.0.4/bin/standalone.sh","-b","0.0.0.0"]
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
FROM centos:centos8 as production
|
||||
# https://gist.githubusercontent.com/mottyc/bcea44c569d6374d125abbb44cf97bb2/raw/067399f6ea4506eeaa08e04c0786fa3d86564697/keycloack-install
|
||||
# https://markandruth.co.uk/2020/10/10/running-systemd-inside-a-centos-8-docker-container
|
||||
# https://mtembo.com/technology/installing-keycloak/
|
||||
#RUN dnf update -y
|
||||
RUN dnf install -y java-1.8.0-openjdk-devel wget curl zip
|
||||
RUN mkdir -p /opt/keycloak
|
||||
RUN wget https://github.com/keycloak/keycloak/releases/download/12.0.4/keycloak-12.0.4.zip -P /opt/keycloak
|
||||
WORKDIR /opt/keycloak
|
||||
RUN unzip keycloak-12.0.4.zip -d /opt/keycloak
|
||||
WORKDIR /opt/keycloak/keycloak-12.0.4
|
||||
|
||||
### Proxy environment. This should be done in keycloak.cli but don't know how
|
||||
RUN sed -i 's/<http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true"\/>/<http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true" proxy-address-forwarding="true"\/>/g' standalone/configuration/standalone.xml
|
||||
# postgres
|
||||
# Download PostgreSQL database drivers
|
||||
#RUN mkdir -p /opt/drivers/jdbc
|
||||
#RUN wget https://jdbc.postgresql.org/download/postgresql-42.2.16.jar -P /opt/drivers/jdbc
|
||||
#RUN /opt/keycloak/keycloak-12.0.4/bin/jboss-cli.sh -c --commands="module add --name=org.postgresql --dependencies=javax.api,javax.transaction.api --resources=/opt/drivers/jdbc/postgresql-42.2.16.jar"
|
||||
#RUN /subsystem=datasources/jdbc-driver=postgresql:add(driver-name=postgresql,driver-module-name=org.postgresql,driver-class-name=org.postgresql.Driver)
|
||||
#RUN /subsystem=datasources/data-source=KeycloakDS:remove
|
||||
#RUN /subsystem=datasources/data-source=KeycloakDS:add(driver-name=postgresql,enabled=true,use-java-context=true,connection-url="jdbc:postgresql://host:port/database",jndi-name="java:/jboss/datasources/KeycloakDS",user-name=keycloak,password="PASSWORD",max-pool-size=20)
|
||||
|
||||
#RUN standalone.sh -b 0.0.0.0 -Djboss.http.port=9080 -Dkeycloak.import=/keycloak-work/freeipa-realm.json
|
||||
RUN dnf -y install systemd; dnf clean all; \
|
||||
(cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == systemd-tmpfiles-setup.service ] || rm -f $i; done); \
|
||||
rm -f /lib/systemd/system/multi-user.target.wants/*;\
|
||||
rm -f /etc/systemd/system/*.wants/*;\
|
||||
rm -f /lib/systemd/system/local-fs.target.wants/*; \
|
||||
rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
|
||||
rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
|
||||
rm -f /lib/systemd/system/basic.target.wants/*;\
|
||||
rm -f /lib/systemd/system/anaconda.target.wants/*;
|
||||
|
||||
RUN dnf -y install freeipa-client freeipa-admintools jna sssd-dbus
|
||||
RUN wget https://download.copr.fedorainfracloud.org/results/giesen/libunix-dbus-java/epel-8-x86_64/01134406-libunix-dbus-java/libunix-dbus-java-0.8.0-2.el8.x86_64.rpm
|
||||
RUN dnf install -y libunix-dbus-java-0.8.0-2.el8.x86_64.rpm
|
||||
|
||||
ADD docker-entrypoint.sh /
|
||||
RUN chmod 700 /docker-entrypoint.sh
|
||||
CMD ["/docker-entrypoint.sh"]
|
||||
#CMD ["/opt/keycloak/keycloak-12.0.4/bin/standalone.sh","-b","0.0.0.0"]
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
Centos7: Does not register to freeipa. It has systemd
|
||||
Centos8: Missing dbus, sssd and asks for systemd
|
|
@ -0,0 +1,26 @@
|
|||
#!/bin/sh
|
||||
#set -e
|
||||
# https://spoore.wordpress.com/2017/02/21/how-to-setup-keycloi/
|
||||
ipa-client-install \
|
||||
--fixed-primary \
|
||||
--server ipa.$DOMAIN \
|
||||
--domain $DOMAIN \
|
||||
--principal admin \
|
||||
--password freeipafreeipa \
|
||||
--unattended \
|
||||
--no-nisdomain \
|
||||
--force-join \
|
||||
-N # No NTP now
|
||||
|
||||
echo freeipafreeipa|kinit admin
|
||||
ipa service-add HTTP/sso.santantoni.duckdns.org@SANTANTONI.DUCKDNS.ORG
|
||||
ipa-getkeytab -s ipa.santantoni.duckdns.org \
|
||||
-p HTTP/sso.$DOMAIN@$(echo "$DOMAIN" | awk '{ print toupper($0) }') \
|
||||
-k /etc/ipa.keytab
|
||||
echo "Adding admin user"
|
||||
#/usr/sbin/sssd -i -d 4 &
|
||||
#/opt/keycloak/keycloak-12.0.4/bin/federation-ssd-setup.sh
|
||||
/opt/keycloak/keycloak-12.0.4/bin/add-user-keycloak.sh -r master -u admin -p $KK_ADMIN_PWD
|
||||
echo "Starting keycloak"
|
||||
/opt/keycloak/keycloak-12.0.4/bin/standalone.sh -b 0.0.0.0
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>avatar-minio-extension</artifactId>
|
||||
<groupId>com.github.thomasdarimont.keycloak</groupId>
|
||||
<version>1.0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>avatar-minio-extension-bundle</artifactId>
|
||||
<packaging>ear</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.github.thomasdarimont.keycloak</groupId>
|
||||
<artifactId>avatar-minio-extension-module</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-ear-plugin</artifactId>
|
||||
<version>3.0.1</version>
|
||||
<configuration>
|
||||
<earSourceDirectory>src/main/resources</earSourceDirectory>
|
||||
<includeLibInApplicationXml>true</includeLibInApplicationXml>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.wildfly.plugins</groupId>
|
||||
<artifactId>wildfly-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<jboss-deployment-structure>
|
||||
<deployment>
|
||||
<module-alias name="deployment.avatar-minio-extension-bundle"/>
|
||||
</deployment>
|
||||
<sub-deployment name="com.github.thomasdarimont.keycloak-avatar-minio-extension-module-1.0.1.0-SNAPSHOT.jar">
|
||||
<dependencies>
|
||||
<module name="javax.servlet.api"/>
|
||||
<module name="javax.ws.rs.api"/>
|
||||
<module name="org.jboss.resteasy.resteasy-jaxb-provider"/>
|
||||
<module name="org.jboss.resteasy.resteasy-jaxrs"/>
|
||||
<module name="org.jboss.resteasy.resteasy-multipart-provider"/>
|
||||
<module name="org.keycloak.keycloak-core"/>
|
||||
<module name="org.keycloak.keycloak-server-spi"/>
|
||||
<module name="org.keycloak.keycloak-server-spi-private"/>
|
||||
<module name="org.keycloak.keycloak-services"/>
|
||||
</dependencies>
|
||||
</sub-deployment>
|
||||
</jboss-deployment-structure>
|
|
@ -0,0 +1,73 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<application xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/application_7.xsd" version="7">
|
||||
<display-name>avatar-minio-extension-bundle</display-name>
|
||||
<module>
|
||||
<java>com.github.thomasdarimont.keycloak-avatar-minio-extension-module-1.0.1.0-SNAPSHOT.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>io.minio-minio-5.0.6.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.google.http-client-google-http-client-xml-1.20.0.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.google.http-client-google-http-client-1.20.0.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>org.apache.httpcomponents-httpclient-4.0.1.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>org.apache.httpcomponents-httpcore-4.0.1.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>commons-logging-commons-logging-1.1.1.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>commons-codec-commons-codec-1.3.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>xpp3-xpp3-1.1.4c.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.google.guava-guava-25.1-jre.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>org.checkerframework-checker-qual-2.0.0.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.google.errorprone-error_prone_annotations-2.1.3.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.google.j2objc-j2objc-annotations-1.1.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>org.codehaus.mojo-animal-sniffer-annotations-1.14.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.squareup.okhttp3-okhttp-3.7.0.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.squareup.okio-okio-1.12.0.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>joda-time-joda-time-2.7.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.fasterxml.jackson.core-jackson-annotations-2.9.6.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.fasterxml.jackson.core-jackson-core-2.9.6.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.fasterxml.jackson.core-jackson-databind-2.9.6.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.google.code.findbugs-annotations-3.0.1.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>net.jcip-jcip-annotations-1.0.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.google.code.findbugs-jsr305-3.0.1.jar</java>
|
||||
</module>
|
||||
</application>
|
Binary file not shown.
|
@ -0,0 +1,73 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<application xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/application_7.xsd" version="7">
|
||||
<display-name>avatar-minio-extension-bundle</display-name>
|
||||
<module>
|
||||
<java>com.github.thomasdarimont.keycloak-avatar-minio-extension-module-1.0.1.0-SNAPSHOT.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>io.minio-minio-5.0.6.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.google.http-client-google-http-client-xml-1.20.0.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.google.http-client-google-http-client-1.20.0.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>org.apache.httpcomponents-httpclient-4.0.1.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>org.apache.httpcomponents-httpcore-4.0.1.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>commons-logging-commons-logging-1.1.1.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>commons-codec-commons-codec-1.3.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>xpp3-xpp3-1.1.4c.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.google.guava-guava-25.1-jre.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>org.checkerframework-checker-qual-2.0.0.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.google.errorprone-error_prone_annotations-2.1.3.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.google.j2objc-j2objc-annotations-1.1.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>org.codehaus.mojo-animal-sniffer-annotations-1.14.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.squareup.okhttp3-okhttp-3.7.0.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.squareup.okio-okio-1.12.0.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>joda-time-joda-time-2.7.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.fasterxml.jackson.core-jackson-annotations-2.9.6.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.fasterxml.jackson.core-jackson-core-2.9.6.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.fasterxml.jackson.core-jackson-databind-2.9.6.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.google.code.findbugs-annotations-3.0.1.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>net.jcip-jcip-annotations-1.0.jar</java>
|
||||
</module>
|
||||
<module>
|
||||
<java>com.google.code.findbugs-jsr305-3.0.1.jar</java>
|
||||
</module>
|
||||
</application>
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<jboss-deployment-structure>
|
||||
<deployment>
|
||||
<module-alias name="deployment.avatar-minio-extension-bundle"/>
|
||||
</deployment>
|
||||
<sub-deployment name="com.github.thomasdarimont.keycloak-avatar-minio-extension-module-1.0.1.0-SNAPSHOT.jar">
|
||||
<dependencies>
|
||||
<module name="javax.servlet.api"/>
|
||||
<module name="javax.ws.rs.api"/>
|
||||
<module name="org.jboss.resteasy.resteasy-jaxb-provider"/>
|
||||
<module name="org.jboss.resteasy.resteasy-jaxrs"/>
|
||||
<module name="org.jboss.resteasy.resteasy-multipart-provider"/>
|
||||
<module name="org.keycloak.keycloak-core"/>
|
||||
<module name="org.keycloak.keycloak-server-spi"/>
|
||||
<module name="org.keycloak.keycloak-server-spi-private"/>
|
||||
<module name="org.keycloak.keycloak-services"/>
|
||||
</dependencies>
|
||||
</sub-deployment>
|
||||
</jboss-deployment-structure>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<jboss-deployment-structure>
|
||||
<deployment>
|
||||
<module-alias name="deployment.avatar-minio-extension-bundle"/>
|
||||
</deployment>
|
||||
<sub-deployment name="com.github.thomasdarimont.keycloak-avatar-minio-extension-module-1.0.1.0-SNAPSHOT.jar">
|
||||
<dependencies>
|
||||
<module name="javax.servlet.api"/>
|
||||
<module name="javax.ws.rs.api"/>
|
||||
<module name="org.jboss.resteasy.resteasy-jaxb-provider"/>
|
||||
<module name="org.jboss.resteasy.resteasy-jaxrs"/>
|
||||
<module name="org.jboss.resteasy.resteasy-multipart-provider"/>
|
||||
<module name="org.keycloak.keycloak-core"/>
|
||||
<module name="org.keycloak.keycloak-server-spi"/>
|
||||
<module name="org.keycloak.keycloak-server-spi-private"/>
|
||||
<module name="org.keycloak.keycloak-services"/>
|
||||
</dependencies>
|
||||
</sub-deployment>
|
||||
</jboss-deployment-structure>
|
|
@ -0,0 +1,4 @@
|
|||
#Created by Apache Maven 3.6.0
|
||||
groupId=com.github.thomasdarimont.keycloak
|
||||
artifactId=avatar-minio-extension-bundle
|
||||
version=1.0.1.0-SNAPSHOT
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>avatar-minio-extension</artifactId>
|
||||
<groupId>com.github.thomasdarimont.keycloak</groupId>
|
||||
<version>1.0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>avatar-minio-extension-module</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-server-spi</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-server-spi-private</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-services</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.minio</groupId>
|
||||
<artifactId>minio</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
|
@ -0,0 +1,53 @@
|
|||
package com.github.thomasdarimont.keycloak.avatar;
|
||||
|
||||
import com.github.thomasdarimont.keycloak.avatar.storage.AvatarStorageProvider;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.StreamingOutput;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
public abstract class AbstractAvatarResource {
|
||||
protected static final String AVATAR_IMAGE_PARAMETER = "image";
|
||||
|
||||
protected KeycloakSession session;
|
||||
|
||||
public AbstractAvatarResource(KeycloakSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
public AvatarStorageProvider getAvatarStorageProvider() {
|
||||
return lookupAvatarStorageProvider(session);
|
||||
}
|
||||
|
||||
protected AvatarStorageProvider lookupAvatarStorageProvider(KeycloakSession keycloakSession) {
|
||||
return keycloakSession.getProvider(AvatarStorageProvider.class);
|
||||
}
|
||||
|
||||
protected Response badRequest() {
|
||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
||||
}
|
||||
|
||||
protected void saveUserImage(String realmName, String userId, InputStream imageInputStream) {
|
||||
getAvatarStorageProvider().saveAvatarImage(realmName, userId, imageInputStream);
|
||||
}
|
||||
|
||||
protected StreamingOutput fetchUserImage(String realmId, String userId) {
|
||||
return output -> copyStream(getAvatarStorageProvider().loadAvatarImage(realmId, userId), output);
|
||||
}
|
||||
|
||||
private void copyStream(InputStream in, OutputStream out) throws IOException {
|
||||
|
||||
byte[] buffer = new byte[16384];
|
||||
|
||||
int len;
|
||||
while ((len = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, len);
|
||||
}
|
||||
|
||||
out.flush();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
package com.github.thomasdarimont.keycloak.avatar;
|
||||
|
||||
import com.github.thomasdarimont.keycloak.avatar.storage.AvatarStorageProvider;
|
||||
import java.io.InputStream;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.ForbiddenException;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
|
||||
import org.jboss.resteasy.spi.NotFoundException;
|
||||
import org.jboss.resteasy.spi.UnauthorizedException;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.resources.admin.AdminAuth;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
||||
|
||||
public class AvatarAdminResource extends AbstractAvatarResource {
|
||||
private static final Logger logger = Logger.getLogger(AvatarAdminResource.class);
|
||||
|
||||
private AdminPermissionEvaluator realmAuth;
|
||||
|
||||
@Context
|
||||
private AvatarStorageProvider avatarStorageProvider;
|
||||
|
||||
private AppAuthManager authManager;
|
||||
private TokenManager tokenManager;
|
||||
|
||||
@Context
|
||||
private HttpHeaders httpHeaders;
|
||||
|
||||
@Context
|
||||
private ClientConnection clientConnection;
|
||||
|
||||
private AdminAuth auth;
|
||||
|
||||
public AvatarAdminResource(KeycloakSession session) {
|
||||
super(session);
|
||||
authManager = new AppAuthManager();
|
||||
tokenManager = new TokenManager();
|
||||
}
|
||||
|
||||
public void init() {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
auth = authenticateRealmAdminRequest();
|
||||
|
||||
RealmManager realmManager = new RealmManager(session);
|
||||
if (realm == null) throw new NotFoundException("Realm not found.");
|
||||
|
||||
if (!auth.getRealm().equals(realmManager.getKeycloakAdminstrationRealm())
|
||||
&& !auth.getRealm().equals(realm)) {
|
||||
throw new org.keycloak.services.ForbiddenException();
|
||||
}
|
||||
realmAuth = AdminPermissions.evaluator(session, realm, auth);
|
||||
|
||||
session.getContext().setRealm(realm);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{user_id}")
|
||||
@Produces({"image/png", "image/jpeg", "image/gif"})
|
||||
public Response downloadUserAvatarImage(@PathParam("user_id") String userId) {
|
||||
try {
|
||||
canViewUsers();
|
||||
|
||||
return Response.ok(fetchUserImage(session.getContext().getRealm().getName(), userId)).build();
|
||||
} catch (ForbiddenException e) {
|
||||
return Response.status(Response.Status.FORBIDDEN).entity(e.getMessage()).build();
|
||||
} catch (Exception e) {
|
||||
logger.error("error getting user avatar", e);
|
||||
return Response.serverError().entity(e.getMessage()).build();
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@NoCache
|
||||
@Path("/{user_id}")
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
public Response uploadUserAvatarImage(@PathParam("user_id") String userId, MultipartFormDataInput input) {
|
||||
try {
|
||||
if (auth == null) {
|
||||
return Response.status(Response.Status.UNAUTHORIZED).build();
|
||||
}
|
||||
canManageUsers();
|
||||
|
||||
String realmName = session.getContext().getRealm().getName();
|
||||
|
||||
InputStream imageInputStream = input.getFormDataPart(AVATAR_IMAGE_PARAMETER, InputStream.class, null);
|
||||
saveUserImage(realmName, userId, imageInputStream);
|
||||
|
||||
} catch (ForbiddenException e) {
|
||||
return Response.status(Response.Status.FORBIDDEN).entity(e.getMessage()).build();
|
||||
} catch (Exception e) {
|
||||
logger.error("error saving user avatar", e);
|
||||
return Response.serverError().entity(e.getMessage()).build();
|
||||
}
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
protected AdminAuth authenticateRealmAdminRequest() {
|
||||
String tokenString = authManager.extractAuthorizationHeaderToken(httpHeaders);
|
||||
MultivaluedMap<String, String> queryParameters = session.getContext().getUri().getQueryParameters();
|
||||
if (tokenString == null && queryParameters.containsKey("access_token")) {
|
||||
tokenString = queryParameters.getFirst("access_token");
|
||||
}
|
||||
if (tokenString == null) throw new UnauthorizedException("Bearer");
|
||||
AccessToken token;
|
||||
try {
|
||||
JWSInput input = new JWSInput(tokenString);
|
||||
token = input.readJsonContent(AccessToken.class);
|
||||
} catch (JWSInputException e) {
|
||||
throw new UnauthorizedException("Bearer token format error");
|
||||
}
|
||||
String realmName = token.getIssuer().substring(token.getIssuer().lastIndexOf('/') + 1);
|
||||
RealmManager realmManager = new RealmManager(session);
|
||||
RealmModel realm = realmManager.getRealmByName(realmName);
|
||||
if (realm == null) {
|
||||
throw new UnauthorizedException("Unknown realm in token");
|
||||
}
|
||||
session.getContext().setRealm(realm);
|
||||
AuthenticationManager.AuthResult authResult = authManager.authenticateBearerToken(tokenString, session, realm, session.getContext().getUri(), clientConnection, httpHeaders);
|
||||
if (authResult == null) {
|
||||
logger.debug("Token not valid");
|
||||
throw new UnauthorizedException("Bearer");
|
||||
}
|
||||
|
||||
ClientModel client = realm.getClientByClientId(token.getIssuedFor());
|
||||
if (client == null) {
|
||||
throw new NotFoundException("Could not find client for authorization");
|
||||
|
||||
}
|
||||
|
||||
return new AdminAuth(realm, authResult.getToken(), authResult.getUser(), client);
|
||||
}
|
||||
|
||||
|
||||
private void canViewUsers() {
|
||||
if (!realmAuth.users().canView()) {
|
||||
logger.info("user does not have permission to view users");
|
||||
throw new ForbiddenException("user does not have permission to view users");
|
||||
}
|
||||
}
|
||||
|
||||
private void canManageUsers() {
|
||||
if (!realmAuth.users().canManage()) {
|
||||
logger.info("user does not have permission to manage users");
|
||||
throw new ForbiddenException("user does not have permission to manage users");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
package com.github.thomasdarimont.keycloak.avatar;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
|
||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
public class AvatarResource extends AbstractAvatarResource {
|
||||
private static final Logger log = Logger.getLogger(AvatarResource.class);
|
||||
|
||||
public static final String STATE_CHECKER_ATTRIBUTE = "state_checker";
|
||||
public static final String STATE_CHECKER_PARAMETER = "stateChecker";
|
||||
|
||||
private final AuthenticationManager.AuthResult auth;
|
||||
|
||||
public AvatarResource(KeycloakSession session) {
|
||||
super(session);
|
||||
this.auth = resolveAuthentication(session);
|
||||
}
|
||||
|
||||
private AuthenticationManager.AuthResult resolveAuthentication(KeycloakSession keycloakSession) {
|
||||
AppAuthManager appAuthManager = new AppAuthManager();
|
||||
RealmModel realm = keycloakSession.getContext().getRealm();
|
||||
|
||||
AuthenticationManager.AuthResult authResult = appAuthManager.authenticateIdentityCookie(keycloakSession, realm);
|
||||
if (authResult != null) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Path("/admin")
|
||||
public AvatarAdminResource admin() {
|
||||
AvatarAdminResource service = new AvatarAdminResource(session);
|
||||
ResteasyProviderFactory.getInstance().injectProperties(service);
|
||||
service.init();
|
||||
return service;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces({"image/png", "image/jpeg", "image/gif"})
|
||||
public Response downloadCurrentUserAvatarImage() {
|
||||
|
||||
if (auth == null) {
|
||||
return badRequest();
|
||||
}
|
||||
|
||||
String realmName = auth.getSession().getRealm().getName();
|
||||
String userId = auth.getUser().getId();
|
||||
|
||||
return Response.ok(fetchUserImage(realmName, userId)).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@NoCache
|
||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||
public Response uploadCurrentUserAvatarImage(MultipartFormDataInput input, @Context UriInfo uriInfo) {
|
||||
|
||||
if (auth == null) {
|
||||
return badRequest();
|
||||
}
|
||||
|
||||
if (!isValidStateChecker(input)) {
|
||||
return badRequest();
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
InputStream imageInputStream = input.getFormDataPart(AVATAR_IMAGE_PARAMETER, InputStream.class, null);
|
||||
|
||||
String realmName = auth.getSession().getRealm().getName();
|
||||
String userId = auth.getUser().getId();
|
||||
|
||||
saveUserImage(realmName, userId, imageInputStream);
|
||||
|
||||
if (uriInfo.getQueryParameters().containsKey("account")) {
|
||||
return Response.seeOther(RealmsResource.accountUrl(session.getContext().getUri().getBaseUriBuilder()).build(realmName)).build();
|
||||
}
|
||||
|
||||
|
||||
return Response.ok().build();
|
||||
|
||||
} catch (Exception ex) {
|
||||
return Response.serverError().build();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidStateChecker(MultipartFormDataInput input) {
|
||||
|
||||
try {
|
||||
String actualStateChecker = input.getFormDataPart(STATE_CHECKER_PARAMETER, String.class, null);
|
||||
String requiredStateChecker = (String) session.getAttribute(STATE_CHECKER_ATTRIBUTE);
|
||||
|
||||
return Objects.equals(requiredStateChecker, actualStateChecker);
|
||||
} catch (Exception ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package com.github.thomasdarimont.keycloak.avatar;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.services.resource.RealmResourceProvider;
|
||||
|
||||
public class AvatarResourceProvider implements RealmResourceProvider {
|
||||
|
||||
private final KeycloakSession keycloakSession;
|
||||
|
||||
public AvatarResourceProvider(KeycloakSession keycloakSession) {
|
||||
this.keycloakSession = keycloakSession;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Object getResource() {
|
||||
return new AvatarResource(keycloakSession);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// NOOP
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package com.github.thomasdarimont.keycloak.avatar;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.services.resource.RealmResourceProvider;
|
||||
import org.keycloak.services.resource.RealmResourceProviderFactory;
|
||||
|
||||
import static org.keycloak.Config.Scope;
|
||||
|
||||
public class AvatarResourceProviderFactory implements RealmResourceProviderFactory {
|
||||
|
||||
private AvatarResourceProvider avatarResourceProvider;
|
||||
|
||||
@Override
|
||||
public RealmResourceProvider create(KeycloakSession keycloakSession) {
|
||||
if (avatarResourceProvider == null) {
|
||||
avatarResourceProvider = new AvatarResourceProvider(keycloakSession);
|
||||
}
|
||||
return avatarResourceProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Scope scope) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory keycloakSessionFactory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "avatar-provider";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package com.github.thomasdarimont.keycloak.avatar.storage;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public interface AvatarStorageProvider extends Provider {
|
||||
|
||||
void saveAvatarImage(String realmName, String userId, InputStream input);
|
||||
|
||||
InputStream loadAvatarImage(String realmId, String userId);
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.github.thomasdarimont.keycloak.avatar.storage;
|
||||
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
public interface AvatarStorageProviderFactory extends ProviderFactory<AvatarStorageProvider> {
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package com.github.thomasdarimont.keycloak.avatar.storage;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
public class AvatarStorageProviderSpi implements Spi {
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "avatar-storage";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return AvatarStorageProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return AvatarStorageProviderFactory.class;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package com.github.thomasdarimont.keycloak.avatar.storage.minio;
|
||||
|
||||
import com.github.thomasdarimont.keycloak.avatar.storage.AvatarStorageProvider;
|
||||
import lombok.extern.jbosslog.JBossLog;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
@JBossLog
|
||||
public class MinioAvatarStorageProvider implements AvatarStorageProvider {
|
||||
|
||||
private final MinioTemplate minioTemplate;
|
||||
|
||||
public MinioAvatarStorageProvider(MinioConfig minioConfig) {
|
||||
this.minioTemplate = new MinioTemplate(minioConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveAvatarImage(String realmName, String userId, InputStream input) {
|
||||
|
||||
String bucketName = minioTemplate.getBucketName(realmName);
|
||||
minioTemplate.ensureBucketExists(bucketName);
|
||||
|
||||
minioTemplate.execute(minioClient -> {
|
||||
minioClient.putObject(bucketName, userId, input, "image/png");
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream loadAvatarImage(String realmName, String userId) {
|
||||
|
||||
String bucketName = minioTemplate.getBucketName(realmName);
|
||||
|
||||
return minioTemplate.execute(minioClient -> minioClient.getObject(bucketName, userId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package com.github.thomasdarimont.keycloak.avatar.storage.minio;
|
||||
|
||||
import com.github.thomasdarimont.keycloak.avatar.storage.AvatarStorageProvider;
|
||||
import com.github.thomasdarimont.keycloak.avatar.storage.AvatarStorageProviderFactory;
|
||||
import lombok.extern.jbosslog.JBossLog;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
@JBossLog
|
||||
public class MinioAvatarStorageProviderFactory implements AvatarStorageProviderFactory {
|
||||
|
||||
//TODO remove default settings
|
||||
private static final String DEFAULT_SERVER_URL = "http://isard-sso-avatars:9000";
|
||||
private static final String DEFAULT_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE";
|
||||
private static final String DEFAULT_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
|
||||
|
||||
private MinioConfig minioConfig;
|
||||
|
||||
@Override
|
||||
public AvatarStorageProvider create(KeycloakSession session) {
|
||||
return new MinioAvatarStorageProvider(minioConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
String serverUrl = config.get("server-url", DEFAULT_SERVER_URL);
|
||||
String accessKey = config.get("access-key", DEFAULT_ACCESS_KEY);
|
||||
String secretKey = config.get("secret-key", DEFAULT_SECRET_KEY);
|
||||
|
||||
this.minioConfig = new MinioConfig(serverUrl, accessKey, secretKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "avatar-storage-minio";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.github.thomasdarimont.keycloak.avatar.storage.minio;
|
||||
|
||||
import io.minio.MinioClient;
|
||||
|
||||
public interface MinioCallback<T> {
|
||||
|
||||
T doInMinio(MinioClient minioClient) throws Exception;
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.github.thomasdarimont.keycloak.avatar.storage.minio;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class MinioConfig {
|
||||
|
||||
private final String serverUrl;
|
||||
|
||||
private final String accessKey;
|
||||
|
||||
private final String secretKey;
|
||||
|
||||
private String defaultBucketSuffix = "-avatars";
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package com.github.thomasdarimont.keycloak.avatar.storage.minio;
|
||||
|
||||
import io.minio.MinioClient;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.jbosslog.JBossLog;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
@JBossLog
|
||||
public class MinioTemplate {
|
||||
private static final int TIMEOUT_SECONDS = 15;
|
||||
|
||||
private final MinioConfig minioConfig;
|
||||
|
||||
private OkHttpClient httpClient;
|
||||
|
||||
public MinioTemplate(MinioConfig minioConfig) {
|
||||
this.minioConfig = minioConfig;
|
||||
httpClient = new OkHttpClient.Builder()
|
||||
.connectTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.readTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.writeTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
public <T> T execute(MinioCallback<T> callback) {
|
||||
|
||||
try {
|
||||
MinioClient minioClient = new MinioClient(minioConfig.getServerUrl(), 0, minioConfig.getAccessKey(),
|
||||
minioConfig.getSecretKey(), "", false, httpClient);
|
||||
return callback.doInMinio(minioClient);
|
||||
} catch (Exception mex) {
|
||||
throw new RuntimeException(mex);
|
||||
}
|
||||
}
|
||||
|
||||
public void ensureBucketExists(String bucketName) {
|
||||
|
||||
execute(minioClient -> {
|
||||
|
||||
boolean exists = minioClient.bucketExists(bucketName);
|
||||
if (exists) {
|
||||
log.debugf("Bucket: %s already exists", bucketName);
|
||||
} else {
|
||||
minioClient.makeBucket(bucketName);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public String getBucketName(String realmName) {
|
||||
return realmName + minioConfig.getDefaultBucketSuffix();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"themes": [
|
||||
{
|
||||
"name": "account-avatar",
|
||||
"types": [
|
||||
"account",
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
com.github.thomasdarimont.keycloak.avatar.storage.minio.MinioAvatarStorageProviderFactory
|
|
@ -0,0 +1 @@
|
|||
com.github.thomasdarimont.keycloak.avatar.storage.AvatarStorageProviderSpi
|
|
@ -0,0 +1 @@
|
|||
com.github.thomasdarimont.keycloak.avatar.AvatarResourceProviderFactory
|
|
@ -0,0 +1,93 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.mainLayout active='account' bodyClass='user'; section>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("editAccountHtmlTitle")}</h2>
|
||||
</div>
|
||||
<div class="col-md-2 subtitle">
|
||||
<span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="${url.accountUrl}" class="form-horizontal" method="post">
|
||||
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
|
||||
|
||||
<#if !realm.registrationEmailAsUsername>
|
||||
<div class="form-group ${messagesPerField.printIfExists('username','has-error')}">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="username" class="control-label">${msg("username")}</label> <#if realm.editUsernameAllowed><span class="required">*</span></#if>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
<input type="text" class="form-control" id="username" name="username" <#if !realm.editUsernameAllowed>disabled="disabled"</#if> value="${(account.username!'')}"/>
|
||||
</div>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<div class="form-group ${messagesPerField.printIfExists('email','has-error')}">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="email" class="control-label">${msg("email")}</label> <span class="required">*</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
<input type="text" class="form-control" id="email" name="email" autofocus value="${(account.email!'')}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group ${messagesPerField.printIfExists('firstName','has-error')}">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="firstName" class="control-label">${msg("firstName")}</label> <span class="required">*</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
<input type="text" class="form-control" id="firstName" name="firstName" value="${(account.firstName!'')}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group ${messagesPerField.printIfExists('lastName','has-error')}">
|
||||
<div class="col-sm-2 col-md-2">
|
||||
<label for="lastName" class="control-label">${msg("lastName")}</label> <span class="required">*</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-10 col-md-10">
|
||||
<input type="text" class="form-control" id="lastName" name="lastName" value="${(account.lastName!'')}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
|
||||
<div class="">
|
||||
<#if url.referrerURI??><a href="${url.referrerURI}">${kcSanitize(msg("backToApplication")?no_esc)}</a></#if>
|
||||
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Save">${msg("doSave")}</button>
|
||||
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Cancel">${msg("doCancel")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h2>${msg("changeAvatarHtmlTitle")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<#assign avatarUrl = url.accountUrl?replace("^(.*)(/account/?)(\\?(.*))?$", "$1/avatar-provider/?account&$4", 'r') />
|
||||
<form action="${avatarUrl}" class="form-horizontal" method="post" enctype="multipart/form-data">
|
||||
|
||||
<img src="${avatarUrl}" style="max-width:200px;" >
|
||||
<input type="file" id="avatar" name="image">
|
||||
|
||||
<input type="hidden" name="stateChecker" value="${stateChecker}">
|
||||
|
||||
<div class="form-group">
|
||||
<div id="kc-form-buttons" class="col-md-offset-2 col-md-10 submit">
|
||||
<div class="">
|
||||
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="submitAction" value="Save">${msg("doSave")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</@layout.mainLayout>
|
|
@ -0,0 +1 @@
|
|||
changeAvatarHtmlTitle=Profilbild bearbeiten
|
|
@ -0,0 +1 @@
|
|||
changeAvatarHtmlTitle=Edit Avatar
|
|
@ -0,0 +1 @@
|
|||
parent=keycloak
|
|
@ -0,0 +1,26 @@
|
|||
module.service('UserAvatar', function(Auth) {
|
||||
this.url = function(user, realm) {
|
||||
return authUrl + '/realms/' + realm.realm + '/avatar-provider/admin/' + user.id + "?access_token=" + Auth.authz.token + "&" + + new Date().getTime();
|
||||
}
|
||||
});
|
||||
|
||||
module.controller('UserAvatarCtrl', function($scope, $http, Notifications, UserAvatar) {
|
||||
$scope.avatarUrl = UserAvatar.url($scope.user, $scope.realm);
|
||||
|
||||
$scope.uploadAvatar = function(files) {
|
||||
var fd = new FormData();
|
||||
//Take the first selected file
|
||||
fd.append("image", files[0]);
|
||||
|
||||
$http.post($scope.avatarUrl, fd, {
|
||||
headers: {'Content-Type': undefined },
|
||||
transformRequest: angular.identity
|
||||
}).then(function() {
|
||||
Notifications.success("Your changes have been saved to the user.");
|
||||
$scope.avatarUrl = UserAvatar.url($scope.user, $scope.realm);
|
||||
}, function(error) {
|
||||
console.error(error);
|
||||
Notifications.error("Could not save the avatar");
|
||||
});
|
||||
}
|
||||
});
|
|
@ -0,0 +1,168 @@
|
|||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="#/realms/{{realm.realm}}/users">{{:: 'users' | translate}}</a></li>
|
||||
<li data-ng-hide="create">{{user.username}}</li>
|
||||
<li data-ng-show="create">{{:: 'add-user' | translate}}</li>
|
||||
</ol>
|
||||
|
||||
<kc-tabs-user></kc-tabs-user>
|
||||
|
||||
<form class="form-horizontal" name="userForm" novalidate kc-read-only="!create && !user.access.manage">
|
||||
|
||||
<fieldset class="border-top">
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label"for="id">{{:: 'id' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<input class="form-control" type="text" id="id" name="id" data-ng-model="user.id" autofocus data-ng-readonly="true">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label"for="id">{{:: 'created-at' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
{{user.createdTimestamp|date:'shortDate'}} {{user.createdTimestamp|date:'mediumTime'}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label"for="username">{{:: 'username' | translate}} <span class="required" data-ng-show="create">*</span></label>
|
||||
<div class="col-md-6">
|
||||
<!-- Characters >,<,/,\ are forbidden in username -->
|
||||
<input class="form-control" type="text" id="username" name="username" data-ng-model="user.username" autofocus
|
||||
required ng-pattern="/^[^\<\>\\\/]*$/" data-ng-readonly="!editUsername">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="email">{{:: 'email' | translate}}</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input class="form-control" type="email" name="email" id="email"
|
||||
data-ng-model="user.email">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="firstName">{{:: 'first-name' | translate}}</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input class="form-control" type="text" name="firstName" id="firstName"
|
||||
data-ng-model="user.firstName">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="lastName">{{:: 'last-name' | translate}}</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input class="form-control" type="text" name="lastName" id="lastName"
|
||||
data-ng-model="user.lastName">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group clearfix block">
|
||||
<label class="col-md-2 control-label" for="userEnabled">{{:: 'user-enabled' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<input ng-model="user.enabled" name="userEnabled" id="userEnabled" ng-disabled="!create && !user.access.manage" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'user-enabled.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix block" data-ng-show="realm.bruteForceProtected && !create">
|
||||
<label class="col-md-2 control-label" for="temporarilyDisabled">{{:: 'user-temporarily-locked' | translate}}</label>
|
||||
<div class="col-md-1">
|
||||
<input ng-model="temporarilyDisabled" name="temporarilyDisabled" id="temporarilyDisabled" data-ng-readonly="true" data-ng-disabled="true" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'user-temporarily-locked.tooltip' | translate}}</kc-tooltip>
|
||||
<div class="col-sm-2">
|
||||
<button type="submit" data-ng-click="unlockUser()" data-ng-show="temporarilyDisabled" class="btn btn-default">{{:: 'unlock-user' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group clearfix block" data-ng-show="!create && user.federationLink">
|
||||
<label class="col-md-2 control-label">{{:: 'federation-link' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<a href="{{federationLink}}">{{federationLinkName}}</a>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'user-link.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix block" data-ng-show="!create && user.origin">
|
||||
<label class="col-md-2 control-label">{{:: 'user-origin-link' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<a href="{{originLink}}">{{originName}}</a>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'user-origin.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix block">
|
||||
<label class="col-md-2 control-label" for="emailVerified">{{:: 'email-verified' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<input ng-model="user.emailVerified" name="emailVerified" id="emailVerified" ng-disabled="!create && !user.access.manage" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'email-verified.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix">
|
||||
<label class="col-md-2 control-label" for="reqActions">{{:: 'required-user-actions' | translate}}</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<select ui-select2 id="reqActions" ng-model="user.requiredActions" data-placeholder="{{:: 'select-an-action.placeholder' | translate}}" multiple>
|
||||
<option ng-repeat="action in userReqActionList" value="{{action.alias}}">{{action.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'required-user-actions.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group clearfix" data-ng-if="realm.internationalizationEnabled">
|
||||
<label class="col-md-2 control-label" for="locale">{{:: 'locale' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<select class="form-control" id="locale"
|
||||
ng-model="user.attributes.locale"
|
||||
ng-options="o as o for o in realm.supportedLocales">
|
||||
<option value="" disabled selected>{{:: 'select-one.placeholder' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group clearfix" data-ng-hide="create || !access.impersonation">
|
||||
<label class="col-md-2 control-label" for="impersonate">{{:: 'impersonate-user' | translate}}</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<button id="impersonate" data-ng-show="access.impersonation" kc-read-only-ignore class="btn btn-default" data-ng-click="impersonate()">{{:: 'impersonate' | translate}}</button>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'impersonate-user.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-10 col-md-offset-2" data-ng-show="create && access.manageUsers">
|
||||
<button kc-save data-ng-show="changed">{{:: 'save' | translate}}</button>
|
||||
<button kc-cancel data-ng-click="cancel()">{{:: 'cancel' | translate}}</button>
|
||||
</div>
|
||||
|
||||
<div class="col-md-10 col-md-offset-2" data-ng-show="!create && user.access.manage">
|
||||
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
|
||||
<button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<form class="form-horizontal" ng-controller="UserAvatarCtrl" novalidate>
|
||||
<fieldset class="border-top">
|
||||
<legend><span class="text">Avatar</span></legend>
|
||||
<div class="form-group">
|
||||
<div class="col-md-10 col-md-offset-2">
|
||||
<img style="max-width:600px;" src="{{ avatarUrl }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="image">Upload</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input class="form-control" type="file" name="image" id="image" onchange="angular.element(this).scope().uploadAvatar(this.files)" />
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<kc-menu></kc-menu>
|
|
@ -0,0 +1,2 @@
|
|||
parent=keycloak
|
||||
scripts=js/user-avatar.js
|
Binary file not shown.
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"themes": [
|
||||
{
|
||||
"name": "account-avatar",
|
||||
"types": [
|
||||
"account",
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
com.github.thomasdarimont.keycloak.avatar.storage.minio.MinioAvatarStorageProviderFactory
|
|
@ -0,0 +1 @@
|
|||
com.github.thomasdarimont.keycloak.avatar.storage.AvatarStorageProviderSpi
|
|
@ -0,0 +1 @@
|
|||
com.github.thomasdarimont.keycloak.avatar.AvatarResourceProviderFactory
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue