Skip to content

Commit

Permalink
Merge branch 'master' into PHRAS-4107_release_4.1.11
Browse files Browse the repository at this point in the history
  • Loading branch information
nmaillat authored Dec 12, 2024
2 parents eda291d + 32aa1db commit 8895774
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 24 deletions.
35 changes: 35 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,41 @@ GATEWAY_DENIED_IPS=
# @run
GATEWAY_USERS=



# Manage http incoming request limits by verbs
# this feature is based on ip adresses and need PHRASEANET_TRUSTED_PROXIES
# defined to get real_ip
# READ is for GET and HEAD requests
# WRITE is for POST, PUT, DELETE and PATCH requests
# Enabling the requests Limit
# @run
HTTP_REQUEST_LIMITS=false

# (m) For Exemple 16,000 IP addresses takes 1 megabyte, so our zone can store about 160,000 addresses.
# @run
HTTP_READ_REQUEST_LIMIT_MEMORY=10

# (r/s) Sets the maximum request rate. By default here the rate cannot exceed 10 requests per second
# @run
HTTP_READ_REQUEST_LIMIT_RATE=100

# The burst parameter defines how many requests a client can make in excess of the rate specified
# @run
HTTP_READ_REQUEST_LIMIT_BURST=20

# (m) For Exemple 16,000 IP addresses takes 1 megabyte, so our zone can store about 160,000 addresses.
# @run
HTTP_WRITE_REQUEST_LIMIT_MEMORY=10

# (r/s) Sets the maximum request rate. By default here the rate cannot exceed 10 requests per second
# @run
HTTP_WRITE_REQUEST_LIMIT_RATE=100

# The burst parameter defines how many requests a client can make in excess of the rate specified
# @run
HTTP_WRITE_REQUEST_LIMIT_BURST=20

# https and reverse proxy (on/off)
# set to on in the case : https behind a proxy
# @run
Expand Down
10 changes: 5 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

FROM alchemyfr/phraseanet-base:1.1.0 AS builder
FROM alchemyfr/phraseanet-base:1.2.0 AS builder

COPY --from=composer:2.1.6 /usr/bin/composer /usr/bin/composer

Expand Down Expand Up @@ -72,7 +72,7 @@ CMD []
# Phraseanet install and setup application image
#########################################################################

FROM alchemyfr/phraseanet-base:1.1.0 AS phraseanet-setup
FROM alchemyfr/phraseanet-base:1.2.0 AS phraseanet-setup

COPY --from=builder --chown=app /var/alchemy/Phraseanet /var/alchemy/Phraseanet
ADD ./docker/phraseanet/root /
Expand All @@ -85,7 +85,7 @@ CMD []
# Phraseanet web application image
#########################################################################

FROM alchemyfr/phraseanet-base:1.1.0 AS phraseanet-fpm
FROM alchemyfr/phraseanet-base:1.2.0 AS phraseanet-fpm

COPY --from=builder --chown=app /var/alchemy/Phraseanet /var/alchemy/Phraseanet
ADD ./docker/phraseanet/root /
Expand All @@ -97,7 +97,7 @@ CMD ["php-fpm", "-F"]
# Phraseanet worker application image
#########################################################################

FROM alchemyfr/phraseanet-base:1.1.0 AS phraseanet-worker
FROM alchemyfr/phraseanet-base:1.2.0 AS phraseanet-worker

COPY --from=builder --chown=app /var/alchemy/Phraseanet /var/alchemy/Phraseanet
ADD ./docker/phraseanet/root /
Expand Down Expand Up @@ -139,7 +139,7 @@ HEALTHCHECK CMD wget --spider http://127.0.0.1/login || nginx -s reload || exit
# phraseanet adapted simplesaml service provider
#########################################################################

FROM alchemyfr/phraseanet-base:1.1.0 AS phraseanet-saml-sp
FROM alchemyfr/phraseanet-base:1.2.0 AS phraseanet-saml-sp
RUN apt-get update \
&& apt-get install -y \
apt-transport-https \
Expand Down
8 changes: 8 additions & 0 deletions config/configuration.sample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ authentication:
debug: false
auto-logout: false
auto-connect-idp-name: null
groupmask: "/phraseanet_([^,]+)/i"
fieldmap:
id: sub
login: email
firstname: given_name
lastname: family_name
email: email
groups: group
registration-fields:
-
name: company
Expand Down
23 changes: 17 additions & 6 deletions doc/others/openid-sso.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ authentication:
# logout with phraseanet and also logout with keycloak
auto-logout: true
auto-connect-idp-name: null
groupmask: "/cn=phraseanet_([^,]+),cn=users,ou=alchemy$/i"
fieldmap:
id: sub
login: email
firstname: given_name
lastname: family_name
email: email
groups: group

```

Expand All @@ -47,16 +55,19 @@ authentication:

set the 'Valid post logout redirect URIs' field with `https://{phraseanet-host}/login/logout/` eg: https://phraseanet.phrasea.local/login/logout/

- Choose a client > client scopes > '.... dedicated'

add a 'groups' mapper if not exist, > Add mapper > by configuration

- if not exist create a client scope with mapper type Group Membership
`Mapper type` => Group Membership
`Name` => groups
`Token Claim Name` => groups
`Name` => group
`Token Claim Name` => group
`Full group path` => off
`Add to userinfo` => on

- Add the created client scope to the client

Choose a client > client scopes > Add client scope > choose the scope



#### token expiration
- we can define token expiration in keycloak

Expand Down
8 changes: 8 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ services:
- GATEWAY_DENIED_IPS
- GATEWAY_USERS
- GATEWAY_CSP
- HTTP_REQUEST_LIMITS
- HTTP_READ_REQUEST_LIMIT_MEMORY
- HTTP_READ_REQUEST_LIMIT_RATE
- HTTP_WRITE_REQUEST_LIMIT_MEMORY
- HTTP_WRITE_REQUEST_LIMIT_RATE
- HTTP_READ_REQUEST_LIMIT_BURST
- HTTP_WRITE_REQUEST_LIMIT_BURST

ports:
- ${PHRASEANET_APP_PORT}:80
networks:
Expand Down
10 changes: 9 additions & 1 deletion docker/nginx/root/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,15 @@ else
envsubst < "/securitycontentpolicies.sample.conf" > /etc/nginx/conf.d/securitycontentpolicies.conf
fi

cat /nginx.conf.sample | sed "s/\$MAX_BODY_SIZE/$MAX_BODY_SIZE/g" | sed "s/\$GATEWAY_SEND_TIMEOUT/$GATEWAY_SEND_TIMEOUT/g" | sed "s/\$GATEWAY_FASTCGI_TIMEOUT/$GATEWAY_FASTCGI_TIMEOUT/g" | sed "s/\$MAX_BODY_SIZE/$MAX_BODY_SIZE/g" | sed "s/\$GATEWAY_PROXY_TIMEOUT/$GATEWAY_PROXY_TIMEOUT/g" | sed "s/\$NEW_TARGET/$NEW_TARGET/g" | sed "s/\$NEW_RESOLVER/$NEW_RESOLVER/g" | sed "s/\$GATEWAY_FASTCGI_HTTPS/$GATEWAY_FASTCGI_HTTPS/g" > /etc/nginx/conf.d/default.conf

if [[ $HTTP_REQUEST_LIMITS && $HTTP_REQUEST_LIMITS = true ]] && [[ ! -z $HTTP_READ_REQUEST_LIMIT_MEMORY || ! -z $HTTP_READ_REQUEST_LIMIT_RATE || ! -z $HTTP_READ_REQUEST_LIMIT_BURST || ! -z $HTTP_WRITE_REQUEST_LIMIT_MEMORY || ! -z $HTTP_WRITE_REQUEST_LIMIT_RATE || ! -z $HTTP_WRITE_REQUEST_LIMIT_BURST ]]; then
echo "HTTP_REQUEST_LIMITS is $HTTP_REQUEST_LIMITS"
cat /nginx.request_limits.conf.sample | sed "s/\$MAX_BODY_SIZE/$MAX_BODY_SIZE/g" | sed "s/\$GATEWAY_SEND_TIMEOUT/$GATEWAY_SEND_TIMEOUT/g" | sed "s/\$GATEWAY_FASTCGI_TIMEOUT/$GATEWAY_FASTCGI_TIMEOUT/g" | sed "s/\$MAX_BODY_SIZE/$MAX_BODY_SIZE/g" | sed "s/\$GATEWAY_PROXY_TIMEOUT/$GATEWAY_PROXY_TIMEOUT/g" | sed "s/\$NEW_TARGET/$NEW_TARGET/g" | sed "s/\$NEW_RESOLVER/$NEW_RESOLVER/g" | sed "s/\$GATEWAY_FASTCGI_HTTPS/$GATEWAY_FASTCGI_HTTPS/g" | sed "s/\$HTTP_READ_REQUEST_LIMIT_MEMORY/$HTTP_READ_REQUEST_LIMIT_MEMORY/g" | sed "s/\$HTTP_READ_REQUEST_LIMIT_RATE/$HTTP_READ_REQUEST_LIMIT_RATE/g" | sed "s/\$HTTP_WRITE_REQUEST_LIMIT_MEMORY/$HTTP_WRITE_REQUEST_LIMIT_MEMORY/g" | sed "s/\$HTTP_WRITE_REQUEST_LIMIT_RATE/$HTTP_WRITE_REQUEST_LIMIT_RATE/g" | sed "s/\$HTTP_READ_REQUEST_LIMIT_BURST/$HTTP_READ_REQUEST_LIMIT_BURST/g"| sed "s/\$HTTP_WRITE_REQUEST_LIMIT_BURST/$HTTP_WRITE_REQUEST_LIMIT_BURST/g" > /etc/nginx/conf.d/default.conf
else
echo "HTTP_REQUEST_LIMITS is $HTTP_REQUEST_LIMITS or not defined"
cat /nginx.conf.sample | sed "s/\$MAX_BODY_SIZE/$MAX_BODY_SIZE/g" | sed "s/\$GATEWAY_SEND_TIMEOUT/$GATEWAY_SEND_TIMEOUT/g" | sed "s/\$GATEWAY_FASTCGI_TIMEOUT/$GATEWAY_FASTCGI_TIMEOUT/g" | sed "s/\$MAX_BODY_SIZE/$MAX_BODY_SIZE/g" | sed "s/\$GATEWAY_PROXY_TIMEOUT/$GATEWAY_PROXY_TIMEOUT/g" | sed "s/\$NEW_TARGET/$NEW_TARGET/g" | sed "s/\$NEW_RESOLVER/$NEW_RESOLVER/g" | sed "s/\$GATEWAY_FASTCGI_HTTPS/$GATEWAY_FASTCGI_HTTPS/g" > /etc/nginx/conf.d/default.conf
fi

cat /fastcgi_timeout.conf | sed "s/\$GATEWAY_FASTCGI_TIMEOUT/$GATEWAY_FASTCGI_TIMEOUT/g" > /etc/nginx/fastcgi_extended_params

echo `date +"%Y-%m-%d %H:%M:%S"` " - Setting for real_ip_from using Trusted Proxies"
Expand Down
3 changes: 2 additions & 1 deletion docker/nginx/root/nginx.conf.sample
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@

send_timeout $GATEWAY_SEND_TIMEOUT;
keepalive_timeout $GATEWAY_SEND_TIMEOUT;
proxy_connect_timeout $GATEWAY_PROXY_TIMEOUT;
proxy_send_timeout $GATEWAY_PROXY_TIMEOUT;
client_header_timeout $GATEWAY_SEND_TIMEOUT;
client_body_timeout $GATEWAY_SEND_TIMEOUT;
fastcgi_read_timeout $GATEWAY_FASTCGI_TIMEOUT;

resolver $NEW_RESOLVER;

upstream backend {
Expand Down Expand Up @@ -36,7 +38,6 @@ server {
if (-f /var/alchemy/Phraseanet/datas/nginx/maintenance.html) {
return 503;
}

# First attempt to serve request as file, then
# as directory, then fall back to index.html
try_files $uri $uri/ @rewriteapp;
Expand Down
95 changes: 95 additions & 0 deletions docker/nginx/root/nginx.request_limits.conf.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@

send_timeout $GATEWAY_SEND_TIMEOUT;
keepalive_timeout $GATEWAY_SEND_TIMEOUT;
proxy_connect_timeout $GATEWAY_PROXY_TIMEOUT;
proxy_send_timeout $GATEWAY_PROXY_TIMEOUT;
client_header_timeout $GATEWAY_SEND_TIMEOUT;
client_body_timeout $GATEWAY_SEND_TIMEOUT;
fastcgi_read_timeout $GATEWAY_FASTCGI_TIMEOUT;

map $request_method $postlimit {
default "";
POST $binary_remote_addr;
}

map $request_method $getlimit {
default "";
GET $binary_remote_addr;
}

limit_req_status 429;
limit_req_zone $getlimit zone=readlimitsbyip:$HTTP_READ_REQUEST_LIMIT_MEMORYm rate=$HTTP_READ_REQUEST_LIMIT_RATEr/s;
limit_req_zone $postlimit zone=writelimitsbyip:$HTTP_WRITE_REQUEST_LIMIT_MEMORYm rate=$HTTP_WRITE_REQUEST_LIMIT_RATEr/s;
resolver $NEW_RESOLVER;

upstream backend {
server phraseanet:9000;
}

#upstream samlsp {
# server phraseanet-saml-sp:8080;
#}

server {
listen 80;
root /var/alchemy/Phraseanet/www;

index index.php;
client_max_body_size $MAX_BODY_SIZE;

location /api {
if (-f /var/alchemy/Phraseanet/datas/nginx/maintenance.html) {
return 503;
}
rewrite ^(.*)$ /api.php/$1 last;
}

location / {

error_page 503 = @maintenance;
recursive_error_pages on;
if (-f /var/alchemy/Phraseanet/datas/nginx/maintenance.html) {
return 503;
}
# First attempt to serve request as file, then
# as directory, then fall back to index.html
try_files $uri $uri/ @rewriteapp;
limit_req zone=readlimitsbyip burst=$HTTP_READ_REQUEST_LIMIT_BURST nodelay;
limit_req zone=writelimitsbyip burst=$HTTP_WRITE_REQUEST_LIMIT_BURST nodelay;
}

location @rewriteapp {
rewrite ^(.*)$ /index.php/$1 last;
}

# PHP scripts -> PHP-FPM server listening on 127.0.0.1:9000
location ~ ^/(index|index_dev|api)\.php(/|$) {
fastcgi_pass backend;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
$GATEWAY_FASTCGI_HTTPS
include restrictions;
}

location ~ ^/(status|ping)$ {
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_index index.php;
include fastcgi_params;
include fastcgi_extended_params;
fastcgi_pass backend;
}

location /simplesaml/ {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
set $target $NEW_TARGET:8080;
proxy_pass http://$target;

}

location @maintenance {
root /var/alchemy/Phraseanet/datas/nginx/;
try_files $uri /maintenance.html;
}
}
56 changes: 46 additions & 10 deletions lib/Alchemy/Phrasea/Authentication/Provider/Openid.php
Original file line number Diff line number Diff line change
Expand Up @@ -343,20 +343,26 @@ public function onCallback(Request $request)

$this->debug();

$userName = $data['preferred_username'];
$usegroups = isset($this->config['usegroups']) ? $this->config['usegroups'] : false;
$idKey = isset($this->config['fieldmap']['id']) ? $this->config['fieldmap']['id'] : 'sub';
$loginKey = isset($this->config['fieldmap']['login']) ? $this->config['fieldmap']['login'] : 'email';
$firstnameKey = isset($this->config['fieldmap']['firstname']) ? $this->config['fieldmap']['firstname'] : 'given_name';
$lastnameKey = isset($this->config['fieldmap']['lastname']) ? $this->config['fieldmap']['lastname'] : 'family_name';
$emailKey = isset($this->config['fieldmap']['email']) ? $this->config['fieldmap']['email'] : 'email';
$groupsKey = isset($this->config['fieldmap']['groups']) ? $this->config['fieldmap']['groups'] : 'groups';
$distantUserId = $data['sub'];

if (!\Swift_Validate::email($userName) && isset($data['email'])) {
$userName = $data['email'];// login to be an email
if (!\Swift_Validate::email($data[$loginKey]) && isset($data['email'])) {
$loginKey = 'email';// login to be an email
}

$usegroups = isset($this->config['usegroups']) ? $this->config['usegroups'] : false;
$userUA = $this->CreateUser([
'id' => $distantUserId = $data['sub'],
'login' => $userName,
'firstname' => isset($data['given_name']) ? $data['given_name'] : '',
'lastname' => isset($data['family_name']) ? $data['family_name'] : '' ,
'email' => isset($data['email']) ? $data['email'] : '',
'_groups' => isset($data['groups']) && $usegroups ? $data['groups'] : ''
'id' => $data[$idKey],
'login' => $userName = $data[$loginKey],
'firstname' => isset($data[$firstnameKey]) ? $data[$firstnameKey] : '',
'lastname' => isset($data[$lastnameKey]) ? $data[$lastnameKey] : '' ,
'email' => isset($data[$emailKey]) ? $data[$emailKey] : '',
'_groups' => isset($data[$groupsKey]) && $usegroups ? $this->filterGroups($data[$groupsKey]) : ''
]);

$userAuthProviderRepository = $this->getUsrAuthProviderRepository();
Expand Down Expand Up @@ -715,6 +721,36 @@ private function CreateUser(Array $data)
return $ret;
}

private function filterGroups($groups)
{
$this->debug(sprintf("filtering openid groups :\n%s", print_r($groups, true)));

$ret = [];
if ($this->config['groupmask']) {
$this->debug(sprintf("filtering groups with regexp : \"%s\"", $this->config['groupmask']));
foreach ($groups as $grp) {
$matches = [];
$retpreg = preg_match_all($this->config['groupmask'], $grp, $matches, PREG_SET_ORDER);

$this->debug(sprintf("preg_match('%s', '%s', ...)\n - returned %s \n - matches = %s "
, $this->config['groupmask'], $grp
, print_r($retpreg, true), print_r($matches, true)));

foreach ($matches as $match) {
if (count($match)>0 && isset($match[1]) && !array_key_exists($match[1], $ret)) {
$ret[] = $match[1];
}
}
}
} else {
$this->debug(sprintf("no groupmask defined, openid groups ignored"));
}

$this->debug(sprintf("filtered groups :\n%s", print_r($ret, true)));

return empty($ret) ? '' : $ret ;
}



/**
Expand Down
Loading

0 comments on commit 8895774

Please sign in to comment.