diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6591ec6f4..01ec199c2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,46 @@
# Changelog
+## 1.0.0 (2017-8-31)
+
+This release provides improvements, bug fixes and new features:
+
+- IAM now supports hierarchical groups. The SCIM group management API has been
+ extended to support nested group creation and listing, and the IAM dashboard
+ can now leverage these new API functions (#88)
+- IAM now supports native X.509 authentication (#119) and the ability to
+ link/unlink X.509 certificates to a user membership (#120)
+- IAM now supports configurable on-demand account provisioning for trusted SAML
+ IDPs; this means that the IAM can be configured to automatically on-board
+ users from a trusted IdP/federation after a succesfull external
+ authentication (i.e. no former registration or administration approval is
+ required to on-board users) (#130)
+- IAM now provides an enhanced token management and revocation API that can be
+ used by IAM administrators to see and revoke active tokens in the system (#121)
+- Account linking can be now be disabled via a configuration option (#142)
+- IAM dashboard now correctly displays valid active access tokens for a user
+ (#112)
+- A problem that caused IAM registration access tokens to expire after the
+ first use has been fixed (#134)
+- IAM now provides an endpoint than can be used to monitor the service
+ connectivity to external service (ie. Google) (#150)
+- Improved SAML metadata handling (#146) and reloading (#115)
+- Account linking can now be disabled via a configuration option (#142)
+- The IAM audit log now provides fine-grained information for many events
+ (#137)
+- The IAM token introspection endpoint now correctly supports HTTP form
+ authentication (#149)
+- Notes in registration requests are now required (#114) to make life easier
+ for VO administrators that wants to understand the reason for a registration
+ request
+- Password reset emails now contain the username of the user that has requested
+ the password reset (#108)
+- A stronger SAML account linking logic is now in place (#116)
+- Starting from this release, we provide RPM and Deb packages (#110) and a
+ puppet module to configure the IAM service (#109)
+- The spring-boot dependency has been updated to version 1.3.8.RELEASE (#144)
+- An issue that prevented access to the token revocation endpoint has been
+ fixed (#159)
+
## 0.6.0 (2017-3-31)
This release provides improvements and bug fixes:
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 000000000..532804b7e
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,135 @@
+#!/usr/bin/env groovy
+
+pipeline {
+
+ agent { label 'maven' }
+
+ options {
+ timeout(time: 1, unit: 'HOURS')
+ buildDiscarder(logRotator(numToKeepStr: '5'))
+ }
+
+ parameters {
+ choice(name: 'RUN_SONAR', choices: 'yes\nno', description: 'Run Sonar static analysis')
+ }
+
+ stages {
+ stage('checkout') {
+ steps {
+ deleteDir()
+ checkout scm
+ stash name: 'code', useDefaultExcludes: false
+ }
+ }
+
+ stage('build') {
+ steps {
+ sh 'mvn -B clean compile'
+ }
+ }
+
+ stage('test') {
+ steps {
+ sh 'mvn -B clean test'
+ }
+
+ post {
+ always {
+ junit '**/target/surefire-reports/TEST-*.xml'
+ }
+ }
+ }
+
+ stage('PR analysis'){
+ when{
+ not {
+ environment name: 'CHANGE_URL', value: ''
+ }
+ }
+ steps {
+ script{
+ def tokens = "${env.CHANGE_URL}".tokenize('/')
+ def organization = tokens[tokens.size()-4]
+ def repo = tokens[tokens.size()-3]
+
+ withCredentials([string(credentialsId: '630f8e6c-0d31-4f96-8d82-a1ef536ef059', variable: 'GITHUB_ACCESS_TOKEN')]) {
+ withSonarQubeEnv{
+ sh """
+ mvn -B -U clean compile sonar:sonar \\
+ -Dsonar.analysis.mode=preview \\
+ -Dsonar.github.pullRequest=${env.CHANGE_ID} \\
+ -Dsonar.github.repository=${organization}/${repo} \\
+ -Dsonar.github.oauth=${GITHUB_ACCESS_TOKEN} \\
+ -Dsonar.host.url=${SONAR_HOST_URL} \\
+ -Dsonar.login=${SONAR_AUTH_TOKEN}
+ """
+ }
+ }
+ }
+ }
+ }
+
+ stage('analysis'){
+ when{
+ expression {
+ return "yes" == "${params.RUN_SONAR}"
+ }
+ anyOf { branch 'master'; branch 'develop' }
+ environment name: 'CHANGE_URL', value: ''
+ }
+ steps {
+ script{
+ def cobertura_opts = 'cobertura:cobertura -Dmaven.test.failure.ignore -DfailIfNoTests=false -Dcobertura.report.format=xml'
+ def checkstyle_opts = 'checkstyle:check -Dcheckstyle.config.location=google_checks.xml'
+
+ withSonarQubeEnv{
+ sh "mvn clean -U ${cobertura_opts} ${checkstyle_opts} ${SONAR_MAVEN_GOAL} -Dsonar.host.url=${SONAR_HOST_URL} -Dsonar.login=${SONAR_AUTH_TOKEN}"
+ }
+ }
+ }
+ }
+
+ stage('package') {
+ steps {
+ sh 'mvn -B -DskipTests=true clean package'
+ archive 'iam-login-service/target/iam-login-service.war'
+ archive 'iam-login-service/target/classes/iam.version.properties'
+ archive 'iam-test-client/target/iam-test-client.jar'
+ stash includes: 'iam-login-service/target/iam-login-service.war,iam-login-service/target/classes/iam.version.properties,iam-test-client/target/iam-test-client.jar', name: 'iam-artifacts'
+ }
+ }
+
+ stage('docker-images') {
+ agent { label 'docker' }
+ steps {
+ deleteDir()
+ unstash 'code'
+ unstash 'iam-artifacts'
+ sh '''
+ sed -i -e 's#iam\\.version#IAM_VERSION#' iam-login-service/target/classes/iam.version.properties
+ source iam-login-service/target/classes/iam.version.properties
+ export IAM_LOGIN_SERVICE_VERSION="v${IAM_VERSION}"
+
+ /bin/bash iam-login-service/docker/build-prod-image.sh
+ /bin/bash iam-login-service/docker/push-prod-image.sh
+ /bin/bash iam-test-client/docker/build-prod-image.sh
+ /bin/bash iam-test-client/docker/push-prod-image.sh
+ '''
+ }
+ }
+ }
+
+ post {
+ success {
+ slackSend channel: "#iam", color: 'good', message: "${env.JOB_NAME} - #${env.BUILD_NUMBER} Success (<${env.BUILD_URL}|Open>)"
+ }
+
+ unstable {
+ slackSend channel: "#iam", color: 'danger', message: "${env.JOB_NAME} - #${env.BUILD_NUMBER} Unstable (<${env.BUILD_URL}|Open>)"
+ }
+
+ failure {
+ slackSend channel: "#iam", color: 'danger', message: "${env.JOB_NAME} - #${env.BUILD_NUMBER} Failure (<${env.BUILD_URL}|Open>)"
+ }
+ }
+}
diff --git a/debian/etc/default/iam-login-service b/debian/etc/default/iam-login-service
new file mode 100644
index 000000000..b9cd24917
--- /dev/null
+++ b/debian/etc/default/iam-login-service
@@ -0,0 +1,37 @@
+# Java VM arguments
+IAM_JAVA_OPTS=-Dspring.profiles.active=prod,registration
+
+# Generic options
+IAM_BASE_URL=https://iam.example.org
+IAM_ISSUER=https://iam.example.org/
+IAM_USE_FORWARDED_HEADERS=true
+#IAM_KEY_STORE_LOCATION=file:///var/lib/indigo/iam-login-service/keystore.jks
+IAM_ORGANISATION_NAME=indigo-dc
+
+# Database connection settings
+IAM_DB_HOST=localhost
+IAM_DB_PORT=3306
+IAM_DB_NAME=iam_login_service
+IAM_DB_USERNAME=iam
+IAM_DB_PASSWORD=iam_login_service
+IAM_DB_VALIDATION_QUERY=SELECT 1
+
+## Google profile settings
+#IAM_GOOGLE_CLIENT_ID=define_me_please
+#IAM_GOOGLE_CLIENT_SECRET=define_me_please
+#IAM_GOOGLE_REDIRECT_URIS=https://iam.example.org/openid_connect_login
+
+## SAML profile settings
+#IAM_SAML_ENTITY_ID=https://localhost
+#IAM_SAML_KEYSTORE=file:///var/lib/indigo/iam/iam-login-service/example.ks
+#IAM_SAML_KEYSTORE_PASSWORD=define_me_please
+#IAM_SAML_KEY_ID=define_me_please
+#IAM_SAML_KEY_PASSWORD=define_me_please
+#IAM_SAML_IDP_METADATA=file:///var/lib/indigo/iam-login-service/example-metadata-sha256.xml
+
+# Notification settings
+#IAM_NOTIFICATION_DISABLE=true
+#IAM_NOTIFICATION_FROM=iam@example.org
+#IAM_NOTIFICATION_TASK_DELAY=5000
+#IAM_NOTIFICATION_ADMIN_ADDRESS=iam-support@example.org
+#IAM_MAIL_HOST=smtp.example.org
diff --git a/debian/lib/systemd/system/iam-login-service.service b/debian/lib/systemd/system/iam-login-service.service
new file mode 100644
index 000000000..8b58d1837
--- /dev/null
+++ b/debian/lib/systemd/system/iam-login-service.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=INDIGO IAM Service
+After=syslog.target network.target
+
+[Service]
+EnvironmentFile=/etc/default/iam-login-service
+WorkingDirectory=/var/lib/indigo/iam-login-service
+ExecStart=/usr/bin/java ${IAM_JAVA_OPTS} -jar iam-login-service.war
+KillMode=process
+User=iam
+
+[Install]
+WantedBy=multi-user.target
diff --git a/docker-compose.yml b/docker-compose.yml
index 10ca7c857..e8ec9b98d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -35,7 +35,7 @@ services:
dockerfile: ./iam-login-service/docker/Dockerfile
environment:
- IAM_JAVA_OPTS: -Xdebug -Xrunjdwp:server=y,transport=dt_socket,suspend=n,address=1044 -Dspring.profiles.active=mysql-test
+ IAM_JAVA_OPTS: -Djava.security.egd=file:/dev/./urandom -Xdebug -Xrunjdwp:server=y,transport=dt_socket,suspend=n,address=1044 -Dspring.profiles.active=mysql-test -Dlogging.level.it.infn.mw.iam.authn.x509=DEBUG
IAM_JAR: /code/iam-login-service/target/iam-login-service.war
IAM_BASE_URL: https://iam.local.io
IAM_ISSUER: https://iam.local.io
diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile
index fbce89f2e..260a83ef0 100644
--- a/docker/nginx/Dockerfile
+++ b/docker/nginx/Dockerfile
@@ -1,6 +1,11 @@
FROM nginx
COPY ./wait-for-it.sh /
RUN chmod +x /wait-for-it.sh
-COPY nginx.conf /etc/nginx/conf.d/default.conf
+RUN apt-get update && apt-get install -y ca-certificates && apt-get clean all
+COPY INFN-CA-2015.pem /usr/local/share/ca-certificates/INFN-CA-2015.crt
+COPY igi-test-ca.pem /usr/local/share/ca-certificates/igi-test-ca.crt
+COPY nginx.conf /etc/nginx/
+COPY iam.conf /etc/nginx/conf.d/default.conf
COPY iam.key.pem /etc/ssl/private/
COPY iam.cert.pem /etc/ssl/certs/
+RUN update-ca-certificates
diff --git a/docker/nginx/INFN-CA-2015.pem b/docker/nginx/INFN-CA-2015.pem
new file mode 100644
index 000000000..fad0bf049
--- /dev/null
+++ b/docker/nginx/INFN-CA-2015.pem
@@ -0,0 +1,33 @@
+-----BEGIN CERTIFICATE-----
+MIIFwjCCA6qgAwIBAgIJALMmAsZ9SSYnMA0GCSqGSIb3DQEBCwUAMEMxCzAJBgNV
+BAYTAklUMQ0wCwYDVQQKEwRJTkZOMSUwIwYDVQQDExxJTkZOIENlcnRpZmljYXRp
+b24gQXV0aG9yaXR5MB4XDTE1MTAwNjEwMjIwNVoXDTMwMTAwNjEwMjIwNVowQzEL
+MAkGA1UEBhMCSVQxDTALBgNVBAoTBElORk4xJTAjBgNVBAMTHElORk4gQ2VydGlm
+aWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
+AQDiXdR7kfK7dqc5tQCDZ3YKD89FizGFho2pBxzddUmjVEbEBeOmG//zK4FmBku8
+3STid3YmYOcMMf8C0nAVGktdjw2hqYVjP+pw7mnmWFog/mNMkw/Q7/avLeoiY8I+
+pJtWKPCbhTZInK59k/KcLs7brauV4+fBBp2vscOpM8j4Y6TH7MAJLsrYddzgxCoE
+IvjZ5cRXcPHDN7n2WhojN70XtlQfhYNjUlSGIoqdVXOEKVBEG74Olg888AGeoFPx
+Sc5FaLlM0GeKLgRYYtDUu8tGMdhMdCTgRT515P36v41P7K4wZGMexRb4l7BMHVNf
+ljlVqjr8L2f2g4Dy21HZDDlFfcoq6VzltcDpF3s8o5/r3eQiGVWTSS1JXJpXLJTc
+dvj4q6hPQEsdkyH2aqcvS06N2XWWG27np0JzVsipAP9WRYyLAJO+ETtwOOvqtakF
+7JrP0Nb6jySRPy/QmfY+jKmwf6hJ3WHq/8/6Gr1VRTq0si+ZC46nY89pYf++QLKk
+cge7uKvddxepoLV93Hx/GMGc96jAtD/R4XcRfRjO/1+9rwBOXZNLeNVoD5eCj+Ad
+NDF1ML/Ya8Gv3AOVJNcyAcM145VbFphZwkSTh3M9DRBKTqyQIBVVAF75cpkU13qa
+dQBQQOhiFAZCSSxLG6Iq0lW5KsfQqHd13XaSorPIV/p80wIDAQABo4G4MIG1MA8G
+A1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRDjE3+7JbK
+6e8KpH3BnQLln72WgDBzBgNVHSMEbDBqgBRDjE3+7JbK6e8KpH3BnQLln72WgKFH
+pEUwQzELMAkGA1UEBhMCSVQxDTALBgNVBAoTBElORk4xJTAjBgNVBAMTHElORk4g
+Q2VydGlmaWNhdGlvbiBBdXRob3JpdHmCCQCzJgLGfUkmJzANBgkqhkiG9w0BAQsF
+AAOCAgEAz0nec0stGy30+hNRN52Ni5YYCMFFoX4aD7LdrWt+MT86i4UFzvPRwvOp
+bPcPC63sjQbP+jePgFXsmEaPkDKuf0x344lNyAgIU+JFWinc4gv4nN5oHfuSXG6J
+UTfYLHaVuPahKeHUUpBOytyOMDRKG+FlGOxQvhnohhjUwBffbu1FIu993+d0w2GC
+9Z4zT+GUKSlviOUYbzctDuG0D8FVWJK7L5SsjFSPSfCJlbWKGmdpDNV2vNzkaHsA
+dQ13WqxE8b0JTHdpS3vsrvfSehY4IG4Fj2HqsDE/dflH3gcJb5l4ls8kcA53YRG2
+NDTjvjdq3tv5AlYJzHKcxq1vhUmVx1vkg1aYNgcV8m8wkPhsnQuTdiQm8EA3ItOO
+RNYawfuVeS021RXwRL290HFIlfwm6imRmlKepGvJBWbrVdrrLCq4s5UPjcxnQnZE
+tapQPUtfV1m9V/T69h5jrfVy1nMM4WWA6MVPljlol1k72jArm+oXvoEvDiNfj2qj
+gfvV03R4GXxP+0EWFXac4tiFFu6YC4Hu7ou38tnnW/nx+xurvnsxIW7ZDaLGKCd+
+VJmb+qhU3NJvDPGjDuksXp0idfhbK6R2dFz7UFS1DYdRit7jeZpou5D4LaIL0CQ/
+KjUrC7M6W+Zhicc0ihbwb03ppLv9/vbj06MY4+HMivKiK1oxd+Q=
+-----END CERTIFICATE-----
diff --git a/docker/nginx/iam.conf b/docker/nginx/iam.conf
new file mode 100644
index 000000000..6ea27e1a8
--- /dev/null
+++ b/docker/nginx/iam.conf
@@ -0,0 +1,44 @@
+server {
+ listen 443 ssl;
+ server_name iam.local.io;
+ access_log /var/log/nginx/iam_local_io.access.log combined_ssl;
+
+ ssl on;
+ ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+ ssl_certificate /etc/ssl/certs/iam.cert.pem;
+ ssl_certificate_key /etc/ssl/private/iam.key.pem;
+ ssl_client_certificate /etc/ssl/certs/ca-certificates.crt;
+ ssl_verify_client optional;
+ ssl_verify_depth 5;
+ ssl_session_cache shared:SSL:10m;
+ ssl_session_timeout 10m;
+
+
+ location / {
+ proxy_pass http://iam-be:8080;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ proxy_set_header Host $http_host;
+
+ # TLS stuff
+
+ proxy_set_header X-SSL-Client-Cert $ssl_client_cert;
+ proxy_set_header X-SSL-Client-I-Dn $ssl_client_i_dn;
+ proxy_set_header X-SSL-Client-S-Dn $ssl_client_s_dn;
+ proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
+ proxy_set_header X-SSL-Client-V-Start $ssl_client_v_start;
+ proxy_set_header X-SSL-Client-V-End $ssl_client_v_end;
+ proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
+ proxy_set_header X-SSL-Protocol $ssl_protocol;
+ proxy_set_header X-SSL-Server-Name $ssl_server_name;
+ }
+
+ location /iam-test-client {
+ proxy_pass http://client:8080/iam-test-client;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ proxy_set_header Host $http_host;
+ }
+}
diff --git a/docker/nginx/igi-test-ca.pem b/docker/nginx/igi-test-ca.pem
new file mode 100644
index 000000000..19906b3bf
--- /dev/null
+++ b/docker/nginx/igi-test-ca.pem
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDgDCCAmigAwIBAgIJAMzDwAv7o5VUMA0GCSqGSIb3DQEBBQUAMC0xCzAJBgNV
+BAYTAklUMQwwCgYDVQQKDANJR0kxEDAOBgNVBAMMB1Rlc3QgQ0EwHhcNMTIwOTI2
+MTUwMDU0WhcNMjIwOTI0MTUwMDU0WjAtMQswCQYDVQQGEwJJVDEMMAoGA1UECgwD
+SUdJMRAwDgYDVQQDDAdUZXN0IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA9u4Fgtj7YpMRql3NAasEUmP6Byv/CH+dPZNzSxfNCMOPqARLBWS/2Ora
+m5cRpoBByT0LpjDCFBJhLrBKvCvmWOTfS1jYsQwSpC/5scButthlcNOhLKQSZblS
+8Pa7HoFS4zQFwCwWOYbOLF+FblYRgSY30WMi361giydeV8iei8KNH2FIoDyo9kjV
+gYQKp76LFv7urGhc5sHA+HWq7+AfyivtZC+a55Rw6EHXOQ+vih5TPXa1t5RL7IkY
+4U7Ld5ExptBIDx0UkSihYexAY4RGXVUaq535dGtJQ8/NYMrJ5NMGt2X0bRszArnE
+EKc/qdAcgcalgoiaZtVkq45eXADXzwIDAQABo4GiMIGfMB0GA1UdDgQWBBSRdzZ7
+LrRp8yfqt/YIi0ojohFJxjBdBgNVHSMEVjBUgBSRdzZ7LrRp8yfqt/YIi0ojohFJ
+xqExpC8wLTELMAkGA1UEBhMCSVQxDDAKBgNVBAoMA0lHSTEQMA4GA1UEAwwHVGVz
+dCBDQYIJAMzDwAv7o5VUMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG
+MA0GCSqGSIb3DQEBBQUAA4IBAQB379cvZmfCLvGdoGbW+6ppDNy3pT9hqYmZAlfV
+FGZSEaTKjGCbPuErUNC6+7zhij5CmMtMRhccI3JswjPHPQGm12jiEC492J6Avj/x
+PL8vcBRofe4whXefDVgUw8G1nkQYr2BF0jzeiN72ToISGMbt/q94QV70lYCo/Tog
+UQQ6F+XhztffxQyRgsUXhR4qq1D4h7UifqfQGBzknS23RMLQUdKXG4MhTLMVmxJC
+uY9Oi0It3hk9Qtn0nlZ7rvo5weJGxuRBbZ85Nvw2tIhH7G2osc6zqmHTmUAR4FXb
+l8/ElwGVrURMMuJLDbISVXjBNFuVOS2BdlyEe4x5kfQAWITZ
+-----END CERTIFICATE-----
diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf
index 2d8f91171..28531501d 100644
--- a/docker/nginx/nginx.conf
+++ b/docker/nginx/nginx.conf
@@ -1,27 +1,37 @@
-server {
- listen 443 ssl;
- server_name iam.local.io;
- access_log /var/log/nginx/iam_local_io.access.log combined;
-
- ssl on;
- ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
- ssl_certificate /etc/ssl/certs/iam.cert.pem;
- ssl_certificate_key /etc/ssl/private/iam.key.pem;
-
- location / {
- proxy_pass http://iam-be:8080;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto https;
- proxy_set_header Host $http_host;
- }
-
- location /iam-test-client {
- proxy_pass http://client:8080/iam-test-client;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto https;
- proxy_set_header Host $http_host;
- }
+user nginx;
+worker_processes 1;
+error_log /var/log/nginx/error.log warn;
+pid /var/run/nginx.pid;
+
+
+events {
+ worker_connections 1024;
+}
+
+http {
+
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ log_format combined_ssl '$remote_addr - $remote_user [$time_local] "$request" '
+ '$ssl_protocol/$ssl_cipher '
+ '"$ssl_client_s_dn" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+
+ sendfile on;
+ #tcp_nopush on;
+
+ keepalive_timeout 65;
+
+ #gzip on;
+
+ include /etc/nginx/conf.d/*.conf;
}
diff --git a/iam-common/pom.xml b/iam-common/pom.xml
index 678ad0c3e..3c699c947 100644
--- a/iam-common/pom.xml
+++ b/iam-common/pom.xml
@@ -7,7 +7,7 @@
it.infn.mw
iam-parent
- 0.6.0
+ 1.0.0
iam-common
diff --git a/iam-login-service/pom.xml b/iam-login-service/pom.xml
index 53d156174..07626ab9a 100644
--- a/iam-login-service/pom.xml
+++ b/iam-login-service/pom.xml
@@ -7,7 +7,7 @@
it.infn.mw
iam-parent
- 0.6.0
+ 1.0.0
iam-login-service
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/actuator/endpoint/ExternalServiceHealthEndpoint.java b/iam-login-service/src/main/java/it/infn/mw/iam/actuator/endpoint/ExternalServiceHealthEndpoint.java
new file mode 100644
index 000000000..a58682624
--- /dev/null
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/actuator/endpoint/ExternalServiceHealthEndpoint.java
@@ -0,0 +1,40 @@
+package it.infn.mw.iam.actuator.endpoint;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.actuate.endpoint.AbstractEndpoint;
+import org.springframework.boot.actuate.health.CompositeHealthIndicator;
+import org.springframework.boot.actuate.health.Health;
+import org.springframework.boot.actuate.health.HealthAggregator;
+import org.springframework.boot.actuate.health.HealthIndicator;
+import org.springframework.boot.actuate.health.OrderedHealthAggregator;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import it.infn.mw.iam.actuator.health.GoogleHealthIndicator;
+
+@Component
+@ConfigurationProperties(prefix = "endpoints.externalService")
+public class ExternalServiceHealthEndpoint extends AbstractEndpoint {
+
+ private static final String ENDPOINT_ID = "externalService";
+
+ private final HealthIndicator healthIndicator;
+
+ @Autowired
+ private HealthAggregator healthAggregator = new OrderedHealthAggregator();
+
+ @Autowired
+ public ExternalServiceHealthEndpoint(GoogleHealthIndicator googleHealthIndicator) {
+ super(ENDPOINT_ID, false);
+
+ CompositeHealthIndicator indicator = new CompositeHealthIndicator(healthAggregator);
+ indicator.addHealthIndicator("google", googleHealthIndicator);
+
+ this.healthIndicator = indicator;
+ }
+
+ @Override
+ public Health invoke() {
+ return this.healthIndicator.health();
+ }
+}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/actuator/endpoint/mvc/ExternalServiceHealthMvcEndpoint.java b/iam-login-service/src/main/java/it/infn/mw/iam/actuator/endpoint/mvc/ExternalServiceHealthMvcEndpoint.java
new file mode 100644
index 000000000..14694fd9d
--- /dev/null
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/actuator/endpoint/mvc/ExternalServiceHealthMvcEndpoint.java
@@ -0,0 +1,77 @@
+package it.infn.mw.iam.actuator.endpoint.mvc;
+
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.actuate.endpoint.mvc.AbstractEndpointMvcAdapter;
+import org.springframework.boot.actuate.health.Health;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.stereotype.Component;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import com.google.common.collect.Maps;
+
+import it.infn.mw.iam.actuator.endpoint.ExternalServiceHealthEndpoint;
+
+@Component
+@ConfigurationProperties(prefix = "endpoints.externalService")
+public class ExternalServiceHealthMvcEndpoint
+ extends AbstractEndpointMvcAdapter {
+
+ private Map statusMapping = Maps.newLinkedHashMap();
+
+ @Autowired
+ public ExternalServiceHealthMvcEndpoint(ExternalServiceHealthEndpoint delegate) {
+ super(delegate);
+ statusMapping.put("DOWN", HttpStatus.SERVICE_UNAVAILABLE);
+ }
+
+ @RequestMapping(produces = APPLICATION_JSON_VALUE)
+ @ResponseBody
+ public Object getServiceHealth(AbstractAuthenticationToken auth) {
+ if (!getDelegate().isEnabled()) {
+ return getDisabledResponse();
+ }
+
+ Health health = getHealth(auth);
+ HttpStatus status = getStatus(health);
+
+ if (status != null) {
+ return new ResponseEntity(health, status);
+ }
+
+ return health;
+ }
+
+
+ private HttpStatus getStatus(Health health) {
+ return statusMapping.get(health.getStatus().getCode());
+ }
+
+ private Health getHealth(AbstractAuthenticationToken auth) {
+ Health health = getDelegate().invoke();
+
+ if (auth != null && isAdmin(auth.getAuthorities())) {
+ return health;
+ }
+ return Health.status(health.getStatus()).build();
+ }
+
+ private boolean isAdmin(Collection authorities) {
+ for (GrantedAuthority authority : authorities) {
+ if ("ROLE_ADMIN".equals(authority.getAuthority())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/actuator/health/GoogleHealthIndicator.java b/iam-login-service/src/main/java/it/infn/mw/iam/actuator/health/GoogleHealthIndicator.java
new file mode 100644
index 000000000..bbb691157
--- /dev/null
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/actuator/health/GoogleHealthIndicator.java
@@ -0,0 +1,40 @@
+package it.infn.mw.iam.actuator.health;
+
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.actuate.health.AbstractHealthIndicator;
+import org.springframework.boot.actuate.health.Health.Builder;
+import org.springframework.stereotype.Component;
+
+@Component
+public class GoogleHealthIndicator extends AbstractHealthIndicator {
+
+ private final String googleEndpoint;
+ private final int timeout;
+
+ @Autowired
+ public GoogleHealthIndicator(@Value("${health.googleEndpoint}") String googleEndpoint,
+ @Value("${health.timeout}") int timeout) {
+ this.googleEndpoint = googleEndpoint;
+ this.timeout = timeout;
+ }
+
+ @Override
+ protected void doHealthCheck(Builder builder) throws Exception {
+ builder.withDetail("location", googleEndpoint);
+
+ HttpURLConnection conn = (HttpURLConnection) new URL(googleEndpoint).openConnection();
+ conn.setRequestMethod("HEAD");
+ conn.setConnectTimeout(timeout);
+ int responseCode = conn.getResponseCode();
+ if (responseCode != 200) {
+ builder.down();
+ } else {
+ builder.up();
+ }
+ }
+
+}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/AccountAuthorityController.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/AccountAuthorityController.java
index 33fef3b17..be96a56d6 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/AccountAuthorityController.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/AccountAuthorityController.java
@@ -54,18 +54,16 @@ protected IamAccount findAccountByName(String name) {
@PreAuthorize("hasRole('USER')")
@RequestMapping(value = "/me/authorities", method = RequestMethod.GET)
public AuthoritySetDTO getAuthoritiesForMe(Authentication authn) {
- AuthoritySetDTO result = AuthoritySetDTO
+ return AuthoritySetDTO
.fromAuthorities(authorityService.getAccountAuthorities(findAccountByName(authn.getName())));
- return result;
}
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping(value = "/account/{id}/authorities", method = RequestMethod.GET)
@ResponseBody
public AuthoritySetDTO getAuthoritiesForAccount(@PathVariable("id") String id) {
- AuthoritySetDTO result = AuthoritySetDTO
+ return AuthoritySetDTO
.fromAuthorities(authorityService.getAccountAuthorities(findAccountById(id)));
- return result;
}
@PreAuthorize("hasRole('ADMIN')")
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/AuthorityAlreadyBoundError.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/AuthorityAlreadyBoundError.java
index bf1f1e9d4..870b2e24c 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/AuthorityAlreadyBoundError.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/AuthorityAlreadyBoundError.java
@@ -6,7 +6,6 @@ public class AuthorityAlreadyBoundError extends RuntimeException {
public AuthorityAlreadyBoundError(String message) {
super(message);
- // TODO Auto-generated constructor stub
}
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/AuthorityDTO.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/AuthorityDTO.java
index 134d0bea6..c37defb19 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/AuthorityDTO.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/AuthorityDTO.java
@@ -10,10 +10,6 @@ public class AuthorityDTO {
@Size(max = 128, message = "Invalid authority size")
private String authority;
- public AuthorityDTO() {
-
- }
-
public String getAuthority() {
return authority;
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/AuthoritySetDTO.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/AuthoritySetDTO.java
index 2112857dd..3fc54ce36 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/AuthoritySetDTO.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/AuthoritySetDTO.java
@@ -6,11 +6,6 @@ public class AuthoritySetDTO {
Set authorities;
- public static AuthoritySetDTO fromAuthorities(Set authorities) {
- return new AuthoritySetDTO(authorities);
- }
-
-
private AuthoritySetDTO(Set authorities) {
this.authorities = authorities;
}
@@ -18,4 +13,8 @@ private AuthoritySetDTO(Set authorities) {
public Set getAuthorities() {
return authorities;
}
+
+ public static AuthoritySetDTO fromAuthorities(Set authorities) {
+ return new AuthoritySetDTO(authorities);
+ }
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/DefaultAccountAuthorityService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/DefaultAccountAuthorityService.java
index 8018c3e2b..502da0811 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/DefaultAccountAuthorityService.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/authority/DefaultAccountAuthorityService.java
@@ -21,6 +21,8 @@
public class DefaultAccountAuthorityService
implements AccountAuthorityService, ApplicationEventPublisherAware {
+ public static final String ACCOUNT_NOT_NULL_MSG = "account must not be null";
+
final IamAuthoritiesRepository authRepo;
final IamAccountRepository accountRepo;
private ApplicationEventPublisher eventPublisher;
@@ -45,7 +47,7 @@ protected IamAuthority findAuthorityFromString(String authority) {
@Override
public void addAuthorityToAccount(IamAccount account, String authority) {
- checkNotNull(account, "account must not be null");
+ checkNotNull(account, ACCOUNT_NOT_NULL_MSG);
IamAuthority iamAuthority = findAuthorityFromString(authority);
@@ -58,30 +60,33 @@ public void addAuthorityToAccount(IamAccount account, String authority) {
account.getAuthorities().add(iamAuthority);
accountRepo.save(account);
- final String message = String.format("Authority %s was added to user %s.",
- authority, account.getUsername());
-
+ final String message =
+ String.format("Authority %s was added to user %s.", authority, account.getUsername());
+
eventPublisher.publishEvent(new AuthorityAddedEvent(this, account, message, authority));
}
@Override
public void removeAuthorityFromAccount(IamAccount account, String authority) {
- checkNotNull(account, "account must not be null");
+ checkNotNull(account, ACCOUNT_NOT_NULL_MSG);
IamAuthority iamAuthority = findAuthorityFromString(authority);
account.getAuthorities().remove(iamAuthority);
accountRepo.save(account);
- final String message =
+ final String message =
String.format("Authority %s was removed from user %s.", authority, account.getUsername());
-
+
eventPublisher.publishEvent(new AuthorityRemovedEvent(this, account, message, authority));
}
@Override
public Set getAccountAuthorities(IamAccount account) {
- checkNotNull(account, "account must not be null");
+ checkNotNull(account, ACCOUNT_NOT_NULL_MSG);
- return account.getAuthorities().stream().map(i -> i.getAuthority()).collect(Collectors.toSet());
+ return account.getAuthorities()
+ .stream()
+ .map(IamAuthority::getAuthority)
+ .collect(Collectors.toSet());
}
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/password_reset/DefaultPasswordResetService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/password_reset/DefaultPasswordResetService.java
index f262c90ed..184db708f 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/password_reset/DefaultPasswordResetService.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/password_reset/DefaultPasswordResetService.java
@@ -1,6 +1,6 @@
package it.infn.mw.iam.api.account.password_reset;
-import java.util.NoSuchElementException;
+import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -65,8 +65,11 @@ public void validateResetToken(String resetToken) {
public void resetPassword(String resetToken, String password) {
validateResetToken(resetToken);
-
- IamAccount account = accountRepository.findByResetKey(resetToken).get();
+ // FIXME: we perform the lookup twice. if validateResetToken
+ // was modified to return the IamAccount we save one call to the DB
+ IamAccount account = accountRepository.findByResetKey(resetToken)
+ .orElseThrow(() -> new InvalidPasswordResetTokenError(
+ String.format("No account found for reset_key [%s]", resetToken)));
eventPublisher.publishEvent(new PasswordResetEvent(this, account,
String.format("User %s reset its password", account.getUsername())));
@@ -80,19 +83,20 @@ public void resetPassword(String resetToken, String password) {
@Override
public void createPasswordResetToken(String email) {
- try {
- IamAccount account = accountRepository.findByEmail(email).get();
-
- if (accountActiveAndEmailVerified(account)) {
- String resetKey = tokenGenerator.generateToken();
- account.setResetKey(resetKey);
- accountRepository.save(account);
-
- notificationService.createResetPasswordMessage(account);
+ Optional accountByMail = accountRepository.findByEmail(email);
+
+ accountByMail.ifPresent(a -> {
+ if (accountActiveAndEmailVerified(a)) {
+ String resetKey = tokenGenerator.generateToken();
+ a.setResetKey(resetKey);
+ accountRepository.save(a);
+ notificationService.createResetPasswordMessage(a);
+ }
+ });
+
+ if (!accountByMail.isPresent()){
+ logger.warn("No account found linked to email: {}", email);
}
- } catch (NoSuchElementException nse) {
- logger.warn("No account found for the email {}. Message: {}", email, nse.getMessage());
- }
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/password_reset/EmailDTO.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/password_reset/EmailDTO.java
index 0ba914a22..f0b76fe55 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/password_reset/EmailDTO.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/password_reset/EmailDTO.java
@@ -10,8 +10,6 @@ public class EmailDTO {
@NotNull(message = "please specify an email address")
private String email;
- public EmailDTO() {}
-
public String getEmail() {
return email;
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/password_reset/PasswordDTO.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/password_reset/PasswordDTO.java
index 172a4c8b4..3c781ff80 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/password_reset/PasswordDTO.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/password_reset/PasswordDTO.java
@@ -11,10 +11,6 @@ public class PasswordDTO {
@NotEmpty(message = "The password cannot be empty")
@Length(min = 5, message = "The password must be at least 5 characters")
private String updatedPassword;
-
- public PasswordDTO() {
-
- }
public String getCurrentPassword() {
return currentPassword;
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/password_reset/PasswordUpdateController.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/password_reset/PasswordUpdateController.java
index 69380f5de..ca2236b92 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/account/password_reset/PasswordUpdateController.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/account/password_reset/PasswordUpdateController.java
@@ -29,9 +29,9 @@
@Transactional
public class PasswordUpdateController {
- public final static String BASE_URL = "/iam/password-update";
- public final static String CURRENT_PASSWORD = "currentPassword";
- public final static String UPDATED_PASSWORD = "updatedPassword";
+ public static final String BASE_URL = "/iam/password-update";
+ public static final String CURRENT_PASSWORD = "currentPassword";
+ public static final String UPDATED_PASSWORD = "updatedPassword";
@Autowired
private PasswordResetService service;
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/account_linking/AccountLinkingConstants.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/account_linking/AccountLinkingConstants.java
new file mode 100644
index 000000000..70caabfaa
--- /dev/null
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/account_linking/AccountLinkingConstants.java
@@ -0,0 +1,5 @@
+package it.infn.mw.iam.api.account_linking;
+
+public interface AccountLinkingConstants {
+ String ACCOUNT_LINKING_DISABLE_PROPERTY = "${accountLinking.disable}";
+}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/account_linking/AccountLinkingController.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/account_linking/AccountLinkingController.java
index 06dea4c03..e23811492 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/account_linking/AccountLinkingController.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/account_linking/AccountLinkingController.java
@@ -1,6 +1,8 @@
package it.infn.mw.iam.api.account_linking;
+import static java.lang.String.format;
+
import java.io.IOException;
import java.security.Principal;
@@ -9,6 +11,7 @@
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
@@ -24,22 +27,73 @@
import it.infn.mw.iam.authn.AbstractExternalAuthenticationToken;
import it.infn.mw.iam.authn.ExternalAuthenticationHandlerSupport;
import it.infn.mw.iam.authn.ExternalAuthenticationRegistrationInfo.ExternalAuthenticationType;
+import it.infn.mw.iam.authn.x509.IamX509AuthenticationCredential;
@Controller
@RequestMapping(AccountLinkingController.ACCCOUNT_LINKING_BASE_RESOURCE)
public class AccountLinkingController extends ExternalAuthenticationHandlerSupport {
final AccountLinkingService linkingService;
+ @Value(ACCOUNT_LINKING_DISABLE_PROPERTY)
+ private Boolean accountLinkingDisabled;
+
@Autowired
public AccountLinkingController(AccountLinkingService s) {
linkingService = s;
}
+
+ @PreAuthorize("hasRole('USER')")
+ @RequestMapping(value = "/X509", method = RequestMethod.DELETE)
+ @ResponseStatus(value = HttpStatus.NO_CONTENT)
+ public void unlinkX509Certificate(Principal principal, @RequestParam String certificateSubject,
+ RedirectAttributes attributes) {
+
+ checkAccountLinkingEnabled(attributes);
+ linkingService.unlinkX509Certificate(principal, certificateSubject);
+ }
+
+
+ @PreAuthorize("hasRole('USER')")
+ @RequestMapping(value = "/X509", method = RequestMethod.POST)
+ public String linkX509Certificate(HttpSession session, Principal principal,
+ RedirectAttributes attributes) {
+
+ clearAccountLinkingSessionAttributes(session);
+ checkAccountLinkingEnabled(attributes);
+
+ try {
+ IamX509AuthenticationCredential cred = getSavedX509AuthenticationCredential(session)
+ .orElseThrow(() -> new IllegalArgumentException(
+ format("No X.509 credential found in session for user '%s'", principal.getName())));
+
+ linkingService.linkX509Certificate(principal, cred);
+ saveX509LinkingSuccess(cred, attributes);
+
+ } catch (Exception ex) {
+ saveAccountLinkingError(ex, attributes);
+ }
+
+ return "redirect:/dashboard";
+ }
+
+
+ private void checkAccountLinkingEnabled(RedirectAttributes attributes) {
+ if (accountLinkingDisabled) {
+ AccountLinkingDisabledException ex = new AccountLinkingDisabledException();
+ saveAccountLinkingError(ex, attributes);
+ throw ex;
+ }
+ }
+
@PreAuthorize("hasRole('USER')")
@RequestMapping(value = "/{type}", method = RequestMethod.POST)
public void linkAccount(@PathVariable ExternalAuthenticationType type,
@RequestParam(value = "id", required = false) String externalIdpId, Authentication authn,
- HttpServletRequest request, HttpServletResponse response) throws IOException {
+ final RedirectAttributes redirectAttributes, HttpServletRequest request,
+ HttpServletResponse response) throws IOException {
+
+ checkAccountLinkingEnabled(redirectAttributes);
HttpSession session = request.getSession();
@@ -56,6 +110,7 @@ public String finalizeAccountLinking(@PathVariable ExternalAuthenticationType ty
Principal principal, final RedirectAttributes redirectAttributes, HttpServletRequest request,
HttpServletResponse response) throws IOException {
+ checkAccountLinkingEnabled(redirectAttributes);
HttpSession session = request.getSession();
if (!hasAccountLinkingDoneKey(session)) {
@@ -90,9 +145,12 @@ public String finalizeAccountLinking(@PathVariable ExternalAuthenticationType ty
@RequestMapping(value = "/{type}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.NO_CONTENT)
public void unlinkAccount(@PathVariable ExternalAuthenticationType type, Principal principal,
- @RequestParam("iss") String issuer, @RequestParam("sub") String subject) {
+ @RequestParam("iss") String issuer, @RequestParam("sub") String subject,
+ @RequestParam(name = "attr", required = false) String attributeId,
+ final RedirectAttributes redirectAttributes) {
- linkingService.unlinkExternalAccount(principal, type, issuer, subject);
+ checkAccountLinkingEnabled(redirectAttributes);
+ linkingService.unlinkExternalAccount(principal, type, issuer, subject, attributeId);
}
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@@ -100,4 +158,10 @@ public void unlinkAccount(@PathVariable ExternalAuthenticationType type, Princip
public String handleIllegalArgumentException(HttpServletRequest request, Exception ex) {
return "iam/dashboard";
}
+
+ @ResponseStatus(value = HttpStatus.FORBIDDEN)
+ @ExceptionHandler(AccountLinkingDisabledException.class)
+ public String handleAccountLinkingDisabledException(HttpServletRequest request, Exception ex) {
+ return "iam/dashboard";
+ }
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/account_linking/AccountLinkingDisabledException.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/account_linking/AccountLinkingDisabledException.java
new file mode 100644
index 000000000..623a36c4c
--- /dev/null
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/account_linking/AccountLinkingDisabledException.java
@@ -0,0 +1,16 @@
+package it.infn.mw.iam.api.account_linking;
+
+public class AccountLinkingDisabledException extends RuntimeException {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1L;
+
+ public static final String MESSAGE = "Account linking is disabled for this IAM instance";
+
+ public AccountLinkingDisabledException() {
+ super(MESSAGE);
+ }
+
+}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/account_linking/AccountLinkingService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/account_linking/AccountLinkingService.java
index 484a86ec5..2073f4a26 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/account_linking/AccountLinkingService.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/account_linking/AccountLinkingService.java
@@ -4,13 +4,19 @@
import it.infn.mw.iam.authn.AbstractExternalAuthenticationToken;
import it.infn.mw.iam.authn.ExternalAuthenticationRegistrationInfo.ExternalAuthenticationType;
+import it.infn.mw.iam.authn.x509.IamX509AuthenticationCredential;
public interface AccountLinkingService {
+ void linkX509Certificate(Principal authenticatedUser,
+ IamX509AuthenticationCredential x509Credential);
+
+ void unlinkX509Certificate(Principal authenticatedUser, String certificateSubject);
+
void linkExternalAccount(Principal authenticatedUser,
AbstractExternalAuthenticationToken> externalAuthenticationToken);
void unlinkExternalAccount(Principal authenticatedUser, ExternalAuthenticationType type,
- String iss, String sub);
+ String iss, String sub, String attributeId);
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/account_linking/DefaultAccountLinkingService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/account_linking/DefaultAccountLinkingService.java
index 7b30e54aa..7f22015b4 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/account_linking/DefaultAccountLinkingService.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/account_linking/DefaultAccountLinkingService.java
@@ -1,8 +1,11 @@
package it.infn.mw.iam.api.account_linking;
import static it.infn.mw.iam.authn.ExternalAuthenticationRegistrationInfo.ExternalAuthenticationType.SAML;
+import static java.lang.String.format;
import java.security.Principal;
+import java.util.Date;
+import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
@@ -12,12 +15,18 @@
import it.infn.mw.iam.audit.events.account.AccountLinkedEvent;
import it.infn.mw.iam.audit.events.account.AccountUnlinkedEvent;
+import it.infn.mw.iam.audit.events.account.X509CertificateLinkedEvent;
+import it.infn.mw.iam.audit.events.account.X509CertificateUnlinkedEvent;
+import it.infn.mw.iam.audit.events.account.X509CertificateUpdatedEvent;
import it.infn.mw.iam.authn.AbstractExternalAuthenticationToken;
import it.infn.mw.iam.authn.ExternalAccountLinker;
import it.infn.mw.iam.authn.ExternalAuthenticationRegistrationInfo.ExternalAuthenticationType;
+import it.infn.mw.iam.authn.error.AccountAlreadyLinkedError;
+import it.infn.mw.iam.authn.x509.IamX509AuthenticationCredential;
import it.infn.mw.iam.persistence.model.IamAccount;
import it.infn.mw.iam.persistence.model.IamOidcId;
import it.infn.mw.iam.persistence.model.IamSamlId;
+import it.infn.mw.iam.persistence.model.IamX509Certificate;
import it.infn.mw.iam.persistence.repository.IamAccountRepository;
@Service
@@ -39,10 +48,9 @@ public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
}
private IamAccount findAccount(Principal authenticatedUser) {
- return iamAccountRepository.findByUsername(authenticatedUser.getName()).orElseThrow(() -> {
- return new UsernameNotFoundException(
- "No user found with username '" + authenticatedUser.getName() + "'");
- });
+ return iamAccountRepository.findByUsername(authenticatedUser.getName())
+ .orElseThrow(() -> new UsernameNotFoundException(
+ "No user found with username '" + authenticatedUser.getName() + "'"));
}
@Override
@@ -54,24 +62,28 @@ public void linkExternalAccount(Principal authenticatedUser,
externalAuthenticationToken.linkToIamAccount(externalAccountLinker, userAccount);
eventPublisher.publishEvent(new AccountLinkedEvent(this, userAccount,
- externalAuthenticationToken.toExernalAuthenticationInfo(),
+ externalAuthenticationToken.toExernalAuthenticationRegistrationInfo(),
String.format("User %s has linked a new account of type %s", userAccount.getUsername(),
- externalAuthenticationToken.toExernalAuthenticationInfo().getType().toString())));
+ externalAuthenticationToken.toExernalAuthenticationRegistrationInfo()
+ .getType()
+ .toString())));
}
@Override
public void unlinkExternalAccount(Principal authenticatedUser, ExternalAuthenticationType type,
- String iss, String sub) {
+ String iss, String sub, String attributeId) {
IamAccount userAccount = findAccount(authenticatedUser);
boolean modified = false;
+
if (SAML.equals(type)) {
IamSamlId id = new IamSamlId();
id.setIdpId(iss);
id.setUserId(sub);
+ id.setAttributeId(attributeId);
userAccount.getSamlIds()
.stream()
@@ -106,4 +118,83 @@ public void unlinkExternalAccount(Principal authenticatedUser, ExternalAuthentic
}
}
+ @Override
+ public void linkX509Certificate(Principal authenticatedUser,
+ IamX509AuthenticationCredential x509Credential) {
+
+ IamAccount userAccount = findAccount(authenticatedUser);
+
+ iamAccountRepository.findByCertificateSubject(x509Credential.getSubject())
+ .ifPresent(linkedAccount -> {
+ if (!linkedAccount.getUuid().equals(userAccount.getUuid())) {
+ throw new AccountAlreadyLinkedError(
+ format("X.509 credential with subject '%s' is already linked to another user",
+ x509Credential.getSubject()));
+ }
+ });
+
+ Optional linkedCert = userAccount.getX509Certificates()
+ .stream()
+ .filter(c -> c.getSubjectDn().equals(x509Credential.getSubject()))
+ .findAny();
+
+ if (linkedCert.isPresent()) {
+
+ linkedCert.ifPresent(c -> {
+ c.setSubjectDn(x509Credential.getSubject());
+ c.setIssuerDn(x509Credential.getIssuer());
+ c.setCertificate(x509Credential.getCertificateChainPemString());
+ c.setLastUpdateTime(new Date());
+ });
+
+ userAccount.touch();
+ iamAccountRepository.save(userAccount);
+
+ eventPublisher.publishEvent(new X509CertificateUpdatedEvent(this, userAccount,
+ String.format("User '%s' has updated its linked certificate with subject '%s'",
+ userAccount.getUsername(), x509Credential.getSubject()),
+ x509Credential));
+
+ } else {
+
+ Date now = new Date();
+ IamX509Certificate newCert = x509Credential.asIamX509Certificate();
+ newCert.setLabel(String.format("cert-%d", userAccount.getX509Certificates().size()));
+
+ newCert.setCreationTime(now);
+ newCert.setLastUpdateTime(now);
+
+ newCert.setPrimary(true);
+ newCert.setAccount(userAccount);
+ userAccount.getX509Certificates().add(newCert);
+ userAccount.touch();
+
+ iamAccountRepository.save(userAccount);
+
+ eventPublisher.publishEvent(new X509CertificateLinkedEvent(this, userAccount,
+ String.format("User '%s' linked certificate with subject '%s' to his/her membership",
+ userAccount.getUsername(), x509Credential.getSubject()),
+ x509Credential));
+
+ }
+ }
+
+ @Override
+ public void unlinkX509Certificate(Principal authenticatedUser, String certificateSubject) {
+ IamAccount userAccount = findAccount(authenticatedUser);
+
+ boolean removed = userAccount.getX509Certificates()
+ .removeIf(c -> c.getSubjectDn().equals(certificateSubject));
+
+ if (removed) {
+ userAccount.touch();
+ iamAccountRepository.save(userAccount);
+
+ eventPublisher.publishEvent(new X509CertificateUnlinkedEvent(this, userAccount,
+ String.format("User '%s' unlinked certificate with subject '%s' from his/her membership",
+ userAccount.getUsername(), certificateSubject),
+ certificateSubject));
+ }
+ }
+
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/authn_info/AuthnInfoController.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/authn_info/AuthnInfoController.java
index 439d72680..88a6b1d65 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/authn_info/AuthnInfoController.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/authn_info/AuthnInfoController.java
@@ -23,7 +23,7 @@ public ExternalAuthenticationRegistrationInfo getAuthenticationInfo() {
(AbstractExternalAuthenticationToken>) SecurityContextHolder.getContext()
.getAuthentication();
- return extAuthnToken.toExernalAuthenticationInfo();
+ return extAuthnToken.toExernalAuthenticationRegistrationInfo();
}
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/controller/ScimControllerSupport.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/controller/ScimControllerSupport.java
new file mode 100644
index 000000000..35924f29d
--- /dev/null
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/controller/ScimControllerSupport.java
@@ -0,0 +1,52 @@
+package it.infn.mw.iam.api.scim.controller;
+
+import it.infn.mw.iam.api.scim.provisioning.paging.DefaultScimPageRequest;
+import it.infn.mw.iam.api.scim.provisioning.paging.ScimPageRequest;
+
+public class ScimControllerSupport {
+
+ protected static final int SCIM_USER_MAX_PAGE_SIZE = 100;
+ protected static final int SCIM_GROUP_MAX_PAGE_SIZE = 10;
+
+ protected ScimPageRequest buildUserPageRequest(Integer count, Integer startIndex) {
+ return buildPageRequest(count, startIndex, SCIM_USER_MAX_PAGE_SIZE);
+ }
+
+ protected ScimPageRequest buildGroupPageRequest(Integer count, Integer startIndex) {
+ return buildPageRequest(count, startIndex, SCIM_GROUP_MAX_PAGE_SIZE);
+ }
+
+ private ScimPageRequest buildPageRequest(Integer count, Integer startIndex, int maxPageSize) {
+
+ int validCount = 0;
+ int validStartIndex = 1;
+
+ if (count == null) {
+ validCount = maxPageSize;
+ } else {
+ validCount = count;
+ if (count < 0) {
+ validCount = 0;
+ } else if (count > maxPageSize) {
+ validCount = maxPageSize;
+ }
+ }
+
+ // SCIM pages index is 1-based
+ if (startIndex == null) {
+ validStartIndex = 1;
+
+ } else {
+
+ validStartIndex = startIndex;
+ if (startIndex < 1) {
+ validStartIndex = 1;
+ }
+ }
+
+ return new DefaultScimPageRequest.Builder().count(validCount)
+ .startIndex(validStartIndex - 1)
+ .build();
+ }
+
+}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/controller/ScimExceptionHandler.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/controller/ScimExceptionHandler.java
index e70807a7a..230cbced5 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/controller/ScimExceptionHandler.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/controller/ScimExceptionHandler.java
@@ -17,6 +17,7 @@
import it.infn.mw.iam.api.scim.exception.ScimResourceNotFoundException;
import it.infn.mw.iam.api.scim.exception.ScimValidationException;
import it.infn.mw.iam.api.scim.model.ScimErrorResponse;
+import it.infn.mw.iam.authn.x509.CertificateParsingError;
import it.infn.mw.iam.util.ssh.InvalidSshKeyException;
@ControllerAdvice
@@ -40,6 +41,14 @@ public ScimErrorResponse handleScimValidationException(ScimValidationException e
return buildErrorResponse(HttpStatus.BAD_REQUEST, e.getMessage());
}
+ @ResponseStatus(code = HttpStatus.BAD_REQUEST)
+ @ExceptionHandler(CertificateParsingError.class)
+ @ResponseBody
+ public ScimErrorResponse handleCertificateParsingError(CertificateParsingError e) {
+
+ return buildErrorResponse(HttpStatus.BAD_REQUEST, e.getMessage());
+ }
+
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/controller/ScimGroupController.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/controller/ScimGroupController.java
index 051def0d8..2e19025b1 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/controller/ScimGroupController.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/controller/ScimGroupController.java
@@ -34,22 +34,21 @@
import it.infn.mw.iam.api.scim.model.ScimGroupPatchRequest;
import it.infn.mw.iam.api.scim.model.ScimListResponse;
import it.infn.mw.iam.api.scim.provisioning.ScimGroupProvisioning;
-import it.infn.mw.iam.api.scim.provisioning.paging.DefaultScimPageRequest;
import it.infn.mw.iam.api.scim.provisioning.paging.ScimPageRequest;
@RestController
@RequestMapping("/scim/Groups")
@Transactional
-public class ScimGroupController {
-
- private static final int SCIM_MAX_PAGE_SIZE = 10;
-
+public class ScimGroupController extends ScimControllerSupport{
+
private Set parseAttributes(final String attributesParameter) {
Set result = new HashSet<>();
if (!Strings.isNullOrEmpty(attributesParameter)) {
- result = Sets.newHashSet(Splitter.on(CharMatcher.anyOf(".,")).trimResults().omitEmptyStrings()
- .split(attributesParameter));
+ result = Sets.newHashSet(Splitter.on(CharMatcher.anyOf(".,"))
+ .trimResults()
+ .omitEmptyStrings()
+ .split(attributesParameter));
}
result.add("schemas");
result.add("id");
@@ -59,27 +58,6 @@ private Set parseAttributes(final String attributesParameter) {
@Autowired
ScimGroupProvisioning groupProvisioningService;
- private ScimPageRequest buildPageRequest(Integer count, Integer startIndex) {
-
- if (count == null || count > SCIM_MAX_PAGE_SIZE) {
- count = SCIM_MAX_PAGE_SIZE;
- }
-
- if (count < 0) {
- count = 0;
- }
-
- // SCIM pages index is 1-based
- if (startIndex == null || startIndex < 1) {
- startIndex = 1;
- }
-
- ScimPageRequest pr =
- new DefaultScimPageRequest.Builder().count(count).startIndex(startIndex - 1).build();
-
- return pr;
- }
-
@PreAuthorize("#oauth2.hasScope('scim:read') or hasRole('ADMIN')")
@RequestMapping(value = "/{id}", method = RequestMethod.GET,
produces = ScimConstants.SCIM_CONTENT_TYPE)
@@ -94,7 +72,7 @@ public MappingJacksonValue listGroups(@RequestParam(required = false) final Inte
@RequestParam(required = false) final Integer startIndex,
@RequestParam(required = false) final String attributes) {
- ScimPageRequest pr = buildPageRequest(count, startIndex);
+ ScimPageRequest pr = buildGroupPageRequest(count, startIndex);
ScimListResponse result = groupProvisioningService.list(pr);
MappingJacksonValue wrapper = new MappingJacksonValue(result);
@@ -119,8 +97,7 @@ public ScimGroup create(@RequestBody @Validated final ScimGroup group,
final BindingResult validationResult) {
handleValidationError("Invalid Scim Group", validationResult);
- ScimGroup result = groupProvisioningService.create(group);
- return result;
+ return groupProvisioningService.create(group);
}
@PreAuthorize("#oauth2.hasScope('scim:write') or hasRole('ADMIN')")
@@ -141,7 +118,8 @@ public ScimGroup replaceGroup(@PathVariable final String id,
consumes = ScimConstants.SCIM_CONTENT_TYPE)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void updateGroup(@PathVariable final String id,
- @RequestBody @Validated final ScimGroupPatchRequest groupPatchRequest, final BindingResult validationResult) {
+ @RequestBody @Validated final ScimGroupPatchRequest groupPatchRequest,
+ final BindingResult validationResult) {
handleValidationError("Invalid Scim Group", validationResult);
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/controller/ScimMeController.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/controller/ScimMeController.java
index b86f770d8..0d441ca4d 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/controller/ScimMeController.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/controller/ScimMeController.java
@@ -2,19 +2,22 @@
import static it.infn.mw.iam.api.scim.controller.utils.ValidationHelper.handleValidationError;
import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REMOVE_OIDC_ID;
+import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REMOVE_PICTURE;
import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REMOVE_SAML_ID;
import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REPLACE_EMAIL;
import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REPLACE_FAMILY_NAME;
import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REPLACE_GIVEN_NAME;
import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REPLACE_PICTURE;
-import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REMOVE_PICTURE;
+import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
@@ -50,9 +53,9 @@
@RestController
@RequestMapping("/scim/Me")
@Transactional
-public class ScimMeController {
+public class ScimMeController implements ApplicationEventPublisherAware {
- public static final EnumSet SUPPORTED_UPDATER_TYPES =
+ protected static final EnumSet SUPPORTED_UPDATER_TYPES =
EnumSet.of(ACCOUNT_REMOVE_OIDC_ID, ACCOUNT_REMOVE_SAML_ID, ACCOUNT_REPLACE_EMAIL,
ACCOUNT_REPLACE_FAMILY_NAME, ACCOUNT_REPLACE_GIVEN_NAME, ACCOUNT_REPLACE_PICTURE,
ACCOUNT_REMOVE_PICTURE);
@@ -63,6 +66,8 @@ public class ScimMeController {
private final DefaultAccountUpdaterFactory updatersFactory;
+ private ApplicationEventPublisher eventPublisher;
+
@Autowired
public ScimMeController(IamAccountRepository accountRepository, UserConverter userConverter,
PasswordEncoder passwordEncoder, OidcIdConverter oidcIdConverter,
@@ -75,8 +80,12 @@ public ScimMeController(IamAccountRepository accountRepository, UserConverter us
oidcIdConverter, samlIdConverter, sshKeyConverter, x509CertificateConverter);
}
+ public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
+ this.eventPublisher = publisher;
+ }
+
@PreAuthorize("#oauth2.hasScope('scim:read') or hasRole('USER')")
- @RequestMapping(method = RequestMethod.GET)
+ @RequestMapping(method = RequestMethod.GET, produces = ScimConstants.SCIM_CONTENT_TYPE)
public ScimUser whoami() {
IamAccount account = getCurrentUserAccount();
@@ -102,6 +111,7 @@ public void updateUser(
private void executePatchOperation(IamAccount account, ScimPatchOperation op) {
List updaters = updatersFactory.getUpdatersForPatchOperation(account, op);
+ List updatesToPublish = new ArrayList<>();
boolean hasChanged = false;
@@ -111,6 +121,7 @@ private void executePatchOperation(IamAccount account, ScimPatchOperation parseAttributes(final String attributesParameter) {
Set result = new HashSet<>();
if (!Strings.isNullOrEmpty(attributesParameter)) {
- result = Sets.newHashSet(Splitter.on(CharMatcher.anyOf(".,")).trimResults().omitEmptyStrings()
- .split(attributesParameter));
+ result = Sets.newHashSet(Splitter.on(CharMatcher.anyOf(".,"))
+ .trimResults()
+ .omitEmptyStrings()
+ .split(attributesParameter));
}
result.add("schemas");
result.add("id");
return result;
}
-
- private ScimPageRequest buildPageRequest(Integer count, Integer startIndex) {
-
- if (count == null || count > SCIM_MAX_PAGE_SIZE) {
- count = SCIM_MAX_PAGE_SIZE;
- }
-
- if (count < 0) {
- count = 0;
- }
-
- // SCIM pages index is 1-based
- if (startIndex == null || startIndex < 1) {
- startIndex = 1;
- }
-
- ScimPageRequest pr =
- new DefaultScimPageRequest.Builder().count(count).startIndex(startIndex - 1).build();
-
- return pr;
- }
-
@PreAuthorize("#oauth2.hasScope('scim:read') or hasRole('ADMIN')")
@RequestMapping(method = RequestMethod.GET, produces = ScimConstants.SCIM_CONTENT_TYPE)
public MappingJacksonValue listUsers(@RequestParam(required = false) final Integer count,
@RequestParam(required = false) final Integer startIndex,
@RequestParam(required = false) final String attributes) {
- ScimPageRequest pr = buildPageRequest(count, startIndex);
+ ScimPageRequest pr = buildUserPageRequest(count, startIndex);
ScimListResponse result = userProvisioningService.list(pr);
MappingJacksonValue wrapper = new MappingJacksonValue(result);
@@ -126,9 +103,7 @@ public MappingJacksonValue create(
handleValidationError("Invalid Scim User", validationResult);
ScimUser result = userProvisioningService.create(user);
- MappingJacksonValue wrapper = new MappingJacksonValue(result);
-
- return wrapper;
+ return new MappingJacksonValue(result);
}
@PreAuthorize("#oauth2.hasScope('scim:write') or hasRole('ADMIN')")
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/AddressConverter.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/AddressConverter.java
index a1e042994..4c448db0b 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/AddressConverter.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/AddressConverter.java
@@ -28,12 +28,14 @@ public Address fromScim(ScimAddress scim) {
@Override
public ScimAddress toScim(Address entity) {
- ScimAddress address =
- ScimAddress.builder().country(entity.getCountry()).formatted(entity.getFormatted())
- .locality(entity.getLocality()).postalCode(entity.getPostalCode())
- .region(entity.getRegion()).streetAddress(entity.getStreetAddress()).build();
-
- return address;
+ return ScimAddress.builder()
+ .country(entity.getCountry())
+ .formatted(entity.getFormatted())
+ .locality(entity.getLocality())
+ .postalCode(entity.getPostalCode())
+ .region(entity.getRegion())
+ .streetAddress(entity.getStreetAddress())
+ .build();
}
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/SamlIdConverter.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/SamlIdConverter.java
index 6f5ef8a32..84b431078 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/SamlIdConverter.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/SamlIdConverter.java
@@ -14,6 +14,7 @@ public IamSamlId fromScim(ScimSamlId scim) {
IamSamlId samlId = new IamSamlId();
samlId.setIdpId(scim.getIdpId());
samlId.setUserId(scim.getUserId());
+ samlId.setAttributeId(scim.getAttributeId());
samlId.setAccount(null);
return samlId;
@@ -22,6 +23,9 @@ public IamSamlId fromScim(ScimSamlId scim) {
@Override
public ScimSamlId toScim(IamSamlId entity) {
- return ScimSamlId.builder().idpId(entity.getIdpId()).userId(entity.getUserId()).build();
+ return ScimSamlId.builder().idpId(entity.getIdpId()).
+ userId(entity.getUserId())
+ .attributeId(entity.getAttributeId())
+ .build();
}
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java
index f37f81eae..f19a2cc84 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/UserConverter.java
@@ -28,22 +28,23 @@ public class UserConverter implements Converter {
private final AddressConverter addressConverter;
- private final X509CertificateConverter x509CertificateConverter;
private final OidcIdConverter oidcIdConverter;
private final SshKeyConverter sshKeyConverter;
private final SamlIdConverter samlIdConverter;
+ private final X509CertificateConverter x509CertificateIamConverter;
@Autowired
- public UserConverter(ScimResourceLocationProvider rlp, X509CertificateConverter x509cc,
- AddressConverter ac, OidcIdConverter oidc, SshKeyConverter sshc, SamlIdConverter samlc) {
+ public UserConverter(ScimResourceLocationProvider rlp, AddressConverter ac,
+ OidcIdConverter oidc, SshKeyConverter sshc, SamlIdConverter samlc,
+ X509CertificateConverter x509Iamcc) {
this.resourceLocationProvider = rlp;
this.addressConverter = ac;
- this.x509CertificateConverter = x509cc;
this.oidcIdConverter = oidc;
this.sshKeyConverter = sshc;
this.samlIdConverter = samlc;
+ this.x509CertificateIamConverter = x509Iamcc;
}
@Override
@@ -63,17 +64,7 @@ public IamAccount fromScim(ScimUser scimUser) {
account.setPassword(scimUser.getPassword());
}
-
- if (scimUser.hasX509Certificates()) {
-
- scimUser.getX509Certificates().forEach(scimCert -> {
-
- IamX509Certificate iamCert = x509CertificateConverter.fromScim(scimCert);
- iamCert.setAccount(account);
- account.getX509Certificates().add(iamCert);
- });
- }
-
+
if (scimUser.hasOidcIds()) {
scimUser.getIndigoUser().getOidcIds().forEach(oidcId -> {
@@ -96,7 +87,7 @@ public IamAccount fromScim(ScimUser scimUser) {
try {
iamSshKey.setFingerprint(RSAPublicKeyUtils.getSHA256Fingerprint(iamSshKey.getValue()));
} catch (InvalidSshKeyException e) {
- throw new ScimException(e.getMessage());
+ throw new ScimException(e.getMessage(),e);
}
}
@@ -121,6 +112,14 @@ public IamAccount fromScim(ScimUser scimUser) {
});
}
+
+ if (scimUser.hasX509Certificates()) {
+ scimUser.getIndigoUser().getCertificates().forEach(c -> {
+ IamX509Certificate cert = x509CertificateIamConverter.fromScim(c);
+ cert.setAccount(account);
+ account.getX509Certificates().add(cert);
+ });
+ }
IamUserInfo userInfo = new IamUserInfo();
@@ -138,6 +137,7 @@ public IamAccount fromScim(ScimUser scimUser) {
}
account.setUserInfo(userInfo);
+ userInfo.setIamAccount(account);
return account;
}
@@ -174,8 +174,6 @@ public ScimUser toScim(IamAccount entity) {
}
entity.getGroups().forEach(group -> builder.addGroupRef(getScimGroupRef(group)));
- entity.getX509Certificates()
- .forEach(cert -> builder.addX509Certificate(x509CertificateConverter.toScim(cert)));
return builder.build();
}
@@ -210,6 +208,9 @@ private ScimIndigoUser getScimIndigoUser(IamAccount entity) {
entity.getSamlIds()
.forEach(samlId -> indigoUserBuilder.addSamlId(samlIdConverter.toScim(samlId)));
+ entity.getX509Certificates()
+ .forEach(cert -> indigoUserBuilder.addCertificate(x509CertificateIamConverter.toScim(cert)));
+
ScimIndigoUser indigoUser = indigoUserBuilder.build();
return indigoUser.isEmpty() ? null : indigoUser;
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/X509CertificateConverter.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/X509CertificateConverter.java
index 719cdb168..6aadaafee 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/X509CertificateConverter.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/converter/X509CertificateConverter.java
@@ -1,40 +1,63 @@
package it.infn.mw.iam.api.scim.converter;
+import java.security.Principal;
+import java.security.cert.X509Certificate;
+
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
-import it.infn.mw.iam.api.scim.exception.ScimValidationException;
+import eu.emi.security.authn.x509.impl.X500NameUtils;
import it.infn.mw.iam.api.scim.model.ScimX509Certificate;
+import it.infn.mw.iam.authn.x509.X509CertificateChainParser;
+import it.infn.mw.iam.authn.x509.X509CertificateChainParsingResult;
import it.infn.mw.iam.persistence.model.IamX509Certificate;
-import it.infn.mw.iam.util.x509.X509Utils;
@Service
public class X509CertificateConverter
implements Converter {
- /**
- *
- * - scim.value => certificate
- * - scim.display => label
- * - scim.primary => primary
- * - scim.certificateSubject => must be extract from certificate
- *
- */
+ private final X509CertificateChainParser parser;
+
+
+ @Autowired
+ public X509CertificateConverter(X509CertificateChainParser parser) {
+ this.parser = parser;
+ }
+
+ private String principalAsRfc2253String(Principal principal) {
+ return X500NameUtils.getPortableRFC2253Form(principal.getName());
+ }
- @Override
- public IamX509Certificate fromScim(ScimX509Certificate scim) throws ScimValidationException {
+
+ private IamX509Certificate parseCertificateFromString(String pemString) {
+ X509CertificateChainParsingResult result = parser.parseChainFromString(pemString);
IamX509Certificate cert = new IamX509Certificate();
+ X509Certificate leafCert = result.getChain()[0];
- cert.setCertificate(scim.getValue());
- cert.setLabel(scim.getDisplay());
+ cert.setSubjectDn(principalAsRfc2253String(leafCert.getSubjectX500Principal()));
+ cert.setIssuerDn(principalAsRfc2253String(leafCert.getIssuerX500Principal()));
+
+ cert.setCertificate(pemString);
+ return cert;
+ }
- if (scim.isPrimary() != null) {
- cert.setPrimary(scim.isPrimary());
+ @Override
+ public IamX509Certificate fromScim(ScimX509Certificate scim) {
+
+ IamX509Certificate cert;
+
+ if (scim.getPemEncodedCertificate() != null) {
+ cert = parseCertificateFromString(scim.getPemEncodedCertificate());
} else {
- cert.setPrimary(false);
+ cert = new IamX509Certificate();
+ cert.setCertificate(scim.getPemEncodedCertificate());
+ cert.setSubjectDn(scim.getSubjectDn());
+ cert.setIssuerDn(scim.getIssuerDn());
}
-
- cert.setCertificateSubject(X509Utils.getCertificateSubject(scim.getValue()));
+
+ cert.setLabel(scim.getDisplay());
+ cert.setPrimary(scim.getPrimary() == null ? false : scim.getPrimary());
return cert;
}
@@ -43,9 +66,14 @@ public IamX509Certificate fromScim(ScimX509Certificate scim) throws ScimValidati
public ScimX509Certificate toScim(IamX509Certificate entity) {
return ScimX509Certificate.builder()
- .primary(entity.isPrimary())
+ .created(entity.getCreationTime())
+ .lastModified(entity.getLastUpdateTime())
.display(entity.getLabel())
- .value(entity.getCertificate())
+ .subjectDn(entity.getSubjectDn())
+ .issuerDn(entity.getIssuerDn())
+ .pemEncodedCertificate(entity.getCertificate())
+ .primary(entity.isPrimary())
.build();
}
+
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/exception/IllegalArgumentException.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/exception/IllegalArgumentException.java
index 28afe78fb..fb705a447 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/exception/IllegalArgumentException.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/exception/IllegalArgumentException.java
@@ -10,4 +10,9 @@ public class IllegalArgumentException extends ScimException {
public IllegalArgumentException(String message) {
super(message);
}
+
+ public IllegalArgumentException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/exception/ScimException.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/exception/ScimException.java
index 5841c44c4..0c1b7cd49 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/exception/ScimException.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/exception/ScimException.java
@@ -7,4 +7,14 @@ public class ScimException extends RuntimeException {
public ScimException(String message) {
super(message);
}
+
+ public ScimException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public ScimException(Throwable cause) {
+ super(cause);
+ }
+
+
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/exception/ScimResourceExistsException.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/exception/ScimResourceExistsException.java
index dfca12690..b1c32092c 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/exception/ScimResourceExistsException.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/exception/ScimResourceExistsException.java
@@ -8,4 +8,9 @@ public class ScimResourceExistsException extends ScimException {
public ScimResourceExistsException(String s) {
super(s);
}
+
+ public ScimResourceExistsException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/exception/ScimValidationException.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/exception/ScimValidationException.java
index 9c781a8df..44fbb3319 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/exception/ScimValidationException.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/exception/ScimValidationException.java
@@ -11,4 +11,8 @@ public ScimValidationException(String message) {
super(message);
}
+ public ScimValidationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAddress.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAddress.java
index 2aaafd311..d4168d1a2 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAddress.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimAddress.java
@@ -7,7 +7,7 @@
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class ScimAddress {
- public static enum ScimAddressType {
+ public enum ScimAddressType {
work, home, other;
}
@@ -172,10 +172,6 @@ public static class Builder {
private String country;
private boolean primary = true;
- public Builder() {
-
- }
-
public Builder formatted(String formatted) {
this.formatted = formatted;
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimEmail.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimEmail.java
index df2b9101f..a14509378 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimEmail.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimEmail.java
@@ -14,7 +14,7 @@
public class ScimEmail {
- public static enum ScimEmailType {
+ public enum ScimEmailType {
work, home, other;
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java
index 72edf98b1..abff281ee 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroup.java
@@ -8,6 +8,7 @@
import javax.validation.Valid;
+import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import com.fasterxml.jackson.annotation.JsonCreator;
@@ -24,6 +25,7 @@ public final class ScimGroup extends ScimResource {
public static final String RESOURCE_TYPE = "Group";
@NotBlank
+ @Length(max = 512)
private final String displayName;
@Valid
@@ -63,7 +65,7 @@ public Set getMembers() {
return members;
}
-
+
public ScimIndigoGroup getIndigoGroup() {
return indigoGroup;
}
@@ -76,7 +78,7 @@ public static Builder builder(String groupName) {
public static class Builder extends ScimResource.Builder {
private String displayName;
- private Set members = new HashSet();
+ private Set members = new HashSet<>();
private ScimIndigoGroup indigoGroup = null;
public Builder(String displayName) {
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroupPatchRequest.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroupPatchRequest.java
index 86183b31d..c64bd5fdb 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroupPatchRequest.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroupPatchRequest.java
@@ -56,9 +56,9 @@ public static Builder builder() {
public static class Builder {
- private Set schemas = new HashSet();
+ private Set schemas = new HashSet<>();
private List>> operations =
- new ArrayList>>();
+ new ArrayList<>();
public Builder() {
schemas.add(PATCHOP_SCHEMA);
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroupRef.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroupRef.java
index b2a600149..9dd0152c7 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroupRef.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimGroupRef.java
@@ -43,6 +43,31 @@ public String getRef() {
return ref;
}
+ @Override
+ public int hashCode() {
+ int prime = 31;
+ int result = 1;
+ result = prime * result + ((value == null) ? 0 : value.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ ScimGroupRef other = (ScimGroupRef) obj;
+ if (value == null) {
+ if (other.value != null)
+ return false;
+ } else if (!value.equals(other.value))
+ return false;
+ return true;
+ }
+
public static Builder builder() {
return new Builder();
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimIndigoUser.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimIndigoUser.java
index 3960160a0..503651fd1 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimIndigoUser.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimIndigoUser.java
@@ -3,6 +3,8 @@
import java.util.LinkedList;
import java.util.List;
+import javax.validation.Valid;
+
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
@@ -15,7 +17,8 @@ public enum INDIGO_USER_SCHEMA {
SSH_KEYS(ScimConstants.INDIGO_USER_SCHEMA + ".sshKeys"),
OIDC_IDS(ScimConstants.INDIGO_USER_SCHEMA + ".oidcIds"),
- SAML_IDS(ScimConstants.INDIGO_USER_SCHEMA + ".samlIds");
+ SAML_IDS(ScimConstants.INDIGO_USER_SCHEMA + ".samlIds"),
+ X509_CERTS(ScimConstants.INDIGO_USER_SCHEMA + ".x509Certificates");
private final String text;
@@ -27,20 +30,27 @@ private INDIGO_USER_SCHEMA(String text) {
public String toString() {
return text;
}
- };
+ }
private final List sshKeys;
private final List oidcIds;
+
+ @Valid
private final List samlIds;
+ @Valid
+ private final List certificates;
+
@JsonCreator
private ScimIndigoUser(@JsonProperty("oidcIds") List oidcIds,
@JsonProperty("sshKeys") List sshKeys,
- @JsonProperty("samlIds") List samlIds) {
+ @JsonProperty("samlIds") List samlIds,
+ @JsonProperty("x509Certificates") List certs ) {
- this.oidcIds = oidcIds != null ? oidcIds : new LinkedList();
- this.sshKeys = sshKeys != null ? sshKeys : new LinkedList();
- this.samlIds = samlIds != null ? samlIds : new LinkedList();
+ this.oidcIds = oidcIds != null ? oidcIds : new LinkedList<>();
+ this.sshKeys = sshKeys != null ? sshKeys : new LinkedList<>();
+ this.samlIds = samlIds != null ? samlIds : new LinkedList<>();
+ this.certificates = certs != null ? certs: new LinkedList<>();
}
@@ -48,12 +58,13 @@ private ScimIndigoUser(Builder b) {
this.sshKeys = b.sshKeys;
this.oidcIds = b.oidcIds;
this.samlIds = b.samlIds;
+ this.certificates = b.certificates;
}
@JsonIgnore
public boolean isEmpty() {
- return sshKeys.isEmpty() && oidcIds.isEmpty() && samlIds.isEmpty();
+ return sshKeys.isEmpty() && oidcIds.isEmpty() && samlIds.isEmpty() && certificates.isEmpty();
}
public List getSshKeys() {
@@ -71,6 +82,10 @@ public List getSamlIds() {
return samlIds;
}
+ public List getCertificates() {
+ return certificates;
+ }
+
public static Builder builder() {
return new Builder();
@@ -78,9 +93,10 @@ public static Builder builder() {
public static class Builder {
- private List sshKeys = new LinkedList();
- private List oidcIds = new LinkedList();
- private List samlIds = new LinkedList();
+ private List sshKeys = new LinkedList<>();
+ private List oidcIds = new LinkedList<>();
+ private List samlIds = new LinkedList<>();
+ private List certificates = new LinkedList<>();
public Builder addSshKey(ScimSshKey sshKey) {
@@ -100,8 +116,12 @@ public Builder addSamlId(ScimSamlId samlId) {
return this;
}
+ public Builder addCertificate(ScimX509Certificate cert){
+ certificates.add(cert);
+ return this;
+ }
+
public ScimIndigoUser build() {
-
return new ScimIndigoUser(this);
}
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimName.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimName.java
index 3b38455ee..7be30895d 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimName.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimName.java
@@ -13,10 +13,10 @@
public class ScimName {
interface NewUserValidation {
- };
+ }
interface UpdateUserValidation {
- };
+ }
private final String formatted;
@@ -34,7 +34,7 @@ interface UpdateUserValidation {
private final String honorificSuffix;
@JsonCreator
- private ScimName(@JsonProperty("givenName") String givenName,
+ private ScimName(@JsonProperty("GIVEN_NAME") String givenName,
@JsonProperty("familyName") String familyName, @JsonProperty("middleName") String middleName,
@JsonProperty("honorificPrefix") String honorificPrefix,
@JsonProperty("honorificSuffix") String honorificSuffix) {
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimOidcId.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimOidcId.java
index 3073f353d..198bec4ee 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimOidcId.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimOidcId.java
@@ -65,6 +65,37 @@ public ScimOidcId build() {
}
}
+ @Override
+ public int hashCode() {
+ int prime = 31;
+ int result = 1;
+ result = prime * result + ((issuer == null) ? 0 : issuer.hashCode());
+ result = prime * result + ((subject == null) ? 0 : subject.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ ScimOidcId other = (ScimOidcId) obj;
+ if (issuer == null) {
+ if (other.issuer != null)
+ return false;
+ } else if (!issuer.equals(other.issuer))
+ return false;
+ if (subject == null) {
+ if (other.subject != null)
+ return false;
+ } else if (!subject.equals(other.subject))
+ return false;
+ return true;
+ }
+
@Override
public String toString() {
return "ScimOidcId [issuer=" + issuer + ", subject=" + subject + "]";
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimPatchOperation.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimPatchOperation.java
index d59938568..e473f0cf4 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimPatchOperation.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimPatchOperation.java
@@ -10,7 +10,7 @@
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class ScimPatchOperation {
- public static enum ScimPatchOperationType {
+ public enum ScimPatchOperationType {
add, remove, replace
}
@@ -59,8 +59,6 @@ public static class Builder {
String path;
T value;
- public Builder() {}
-
public Builder path(String path) {
this.path = path;
@@ -93,7 +91,7 @@ public Builder replace() {
public ScimPatchOperation build() {
- return new ScimPatchOperation(this);
+ return new ScimPatchOperation<>(this);
}
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimPhoto.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimPhoto.java
index 46e3ede9e..c6770228a 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimPhoto.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimPhoto.java
@@ -19,7 +19,7 @@ public class ScimPhoto {
@NotNull
private final ScimPhotoType type;
- public static enum ScimPhotoType {
+ public enum ScimPhotoType {
thumbnail, photo;
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimResource.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimResource.java
index 0677c338b..82a3cd192 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimResource.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimResource.java
@@ -77,7 +77,7 @@ public boolean equals(Object obj) {
return true;
}
- public static abstract class Builder {
+ public abstract static class Builder {
protected String externalid;
protected String id;
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimSamlId.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimSamlId.java
index b1810af82..ab297dc96 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimSamlId.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimSamlId.java
@@ -8,6 +8,8 @@
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
+import it.infn.mw.iam.authn.saml.util.Saml2Attribute;
+
@JsonInclude(Include.NON_EMPTY)
public class ScimSamlId {
@@ -19,11 +21,24 @@ public class ScimSamlId {
@Length(max = 256)
private final String userId;
+ @NotBlank
+ @Length(max = 256)
+ private final String attributeId;
+
@JsonCreator
- private ScimSamlId(@JsonProperty("idpId") String idpId, @JsonProperty("userId") String userId) {
+ private ScimSamlId(@JsonProperty("idpId") String idpId, @JsonProperty("userId") String userId,
+ @JsonProperty("attributeId") String attributeId) {
this.userId = userId;
this.idpId = idpId;
+ this.attributeId = attributeId;
+ }
+
+ private ScimSamlId(Builder b) {
+
+ this.idpId = b.idpId;
+ this.userId = b.userId;
+ this.attributeId = b.attributeId;
}
public String getUserId() {
@@ -36,10 +51,8 @@ public String getIdpId() {
return idpId;
}
- private ScimSamlId(Builder b) {
-
- this.idpId = b.idpId;
- this.userId = b.userId;
+ public String getAttributeId() {
+ return attributeId;
}
public static Builder builder() {
@@ -47,10 +60,42 @@ public static Builder builder() {
return new Builder();
}
+ @Override
+ public int hashCode() {
+ int prime = 31;
+ int result = 1;
+ result = prime * result + ((idpId == null) ? 0 : idpId.hashCode());
+ result = prime * result + ((userId == null) ? 0 : userId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ ScimSamlId other = (ScimSamlId) obj;
+ if (idpId == null) {
+ if (other.idpId != null)
+ return false;
+ } else if (!idpId.equals(other.idpId))
+ return false;
+ if (userId == null) {
+ if (other.userId != null)
+ return false;
+ } else if (!userId.equals(other.userId))
+ return false;
+ return true;
+ }
+
public static class Builder {
private String idpId;
private String userId;
+ private String attributeId = Saml2Attribute.EPUID.getAttributeName();
public Builder idpId(String idpId) {
@@ -64,6 +109,11 @@ public Builder userId(String userId) {
return this;
}
+ public Builder attributeId(String attributeId) {
+ this.attributeId = attributeId;
+ return this;
+ }
+
public ScimSamlId build() {
return new ScimSamlId(this);
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimSshKey.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimSshKey.java
index 8ac5692d1..6a467df24 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimSshKey.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimSshKey.java
@@ -76,10 +76,6 @@ public static class Builder {
private Boolean primary;
private String fingerprint;
- public Builder() {
-
- }
-
public Builder display(String display) {
this.display = display;
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java
index 644cb418b..5e4e1675b 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUser.java
@@ -23,10 +23,10 @@
public class ScimUser extends ScimResource {
public interface NewUserValidation {
- };
+ }
public interface UpdateUserValidation {
- };
+ }
public static final String USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User";
public static final String RESOURCE_TYPE = "User";
@@ -56,10 +56,10 @@ public interface UpdateUserValidation {
private final List addresses;
private final List photos;
- private final List x509Certificates;
private final Set groups;
+ @Valid
private final ScimIndigoUser indigoUser;
@JsonCreator
@@ -98,8 +98,6 @@ private ScimUser(@JsonProperty("id") String id, @JsonProperty("externalId") Stri
this.groups = groups;
this.addresses = addresses;
this.indigoUser = indigoUser;
- this.x509Certificates = x509Certificates;
-
}
private ScimUser(Builder b) {
@@ -118,7 +116,6 @@ private ScimUser(Builder b) {
this.active = b.active;
this.emails = b.emails;
this.addresses = b.addresses;
- this.x509Certificates = b.x509Certificates;
this.indigoUser = b.indigoUser;
this.groups = b.groups;
this.password = b.password;
@@ -208,11 +205,6 @@ public List getAddresses() {
return addresses;
}
- public List getX509Certificates() {
-
- return x509Certificates;
- }
-
@JsonProperty(value = ScimConstants.INDIGO_USER_SCHEMA)
public ScimIndigoUser getIndigoUser() {
@@ -226,7 +218,8 @@ public Set getGroups() {
public boolean hasX509Certificates() {
- return x509Certificates != null && !x509Certificates.isEmpty();
+ return indigoUser != null && indigoUser.getCertificates() != null
+ && !indigoUser.getCertificates().isEmpty();
}
public boolean hasOidcIds() {
@@ -282,11 +275,10 @@ public static class Builder extends ScimResource.Builder {
private String timezone;
private Boolean active;
- private List emails = new ArrayList();
- private Set groups = new LinkedHashSet();
- private List addresses = new ArrayList();
- private List photos = new ArrayList();
- private List x509Certificates = new ArrayList();
+ private List emails = new ArrayList<>();
+ private Set groups = new LinkedHashSet<>();
+ private List addresses = new ArrayList<>();
+ private List photos = new ArrayList<>();
private ScimIndigoUser indigoUser;
public Builder() {
@@ -441,14 +433,12 @@ public Builder addX509Certificate(ScimX509Certificate scimX509Certificate) {
Preconditions.checkNotNull(scimX509Certificate, "Null x509 certificate");
- x509Certificates.add(scimX509Certificate);
- return this;
- }
-
- public Builder buildX509Certificate(String display, String value, Boolean isPrimary) {
+ if (indigoUser == null) {
+ indigoUser = ScimIndigoUser.builder().addCertificate(scimX509Certificate).build();
+ } else {
+ indigoUser.getCertificates().add(scimX509Certificate);
+ }
- addX509Certificate(
- ScimX509Certificate.builder().display(display).value(value).primary(isPrimary).build());
return this;
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUserPatchRequest.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUserPatchRequest.java
index fae4718db..efe621a9b 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUserPatchRequest.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimUserPatchRequest.java
@@ -12,7 +12,6 @@
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
-
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
@@ -59,9 +58,9 @@ public static Builder builder() {
public static class Builder {
- private Set schemas = new HashSet();
+ private Set schemas = new HashSet<>();
private List> operations =
- new ArrayList>();
+ new ArrayList<>();
public Builder() {
schemas.add(PATCHOP_SCHEMA);
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimX509Certificate.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimX509Certificate.java
index 72746fd66..9b98ab16a 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimX509Certificate.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/model/ScimX509Certificate.java
@@ -1,91 +1,149 @@
package it.infn.mw.iam.api.scim.model;
+import java.util.Date;
+
import org.hibernate.validator.constraints.Length;
-import org.hibernate.validator.constraints.NotBlank;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+
+import it.infn.mw.iam.api.scim.controller.utils.JsonDateSerializer;
-@JsonInclude(Include.NON_EMPTY)
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class ScimX509Certificate {
@Length(max = 36)
private final String display;
-
+
private final Boolean primary;
+
+ @Length(max = 128)
+ private final String subjectDn;
+
+ @Length(max = 128)
+ private final String issuerDn;
+
+ private final String pemEncodedCertificate;
- @NotBlank
- private final String value;
+ @JsonSerialize(using = JsonDateSerializer.class)
+ private final Date created;
+
+ @JsonSerialize(using = JsonDateSerializer.class)
+ private final Date lastModified;
@JsonCreator
- private ScimX509Certificate(@JsonProperty("display") String display,
- @JsonProperty("primary") Boolean primary, @JsonProperty("value") String value) {
+ private ScimX509Certificate(@JsonProperty("label") String display, @JsonProperty("primary") Boolean primary,
+ @JsonProperty("subjectDn") String subjectDn,
+ @JsonProperty("issuerDn") String issuerDn,
+ @JsonProperty("pemEncodedCertificate") String pemEncodedCertificate) {
this.display = display;
- this.value = value;
this.primary = primary;
+ this.subjectDn = subjectDn;
+ this.issuerDn = issuerDn;
+ this.pemEncodedCertificate = pemEncodedCertificate;
+ this.created = this.lastModified = null;
}
- public String getDisplay() {
+ private ScimX509Certificate(Builder b) {
+ this.display = b.display;
+ this.primary = b.primary;
+ this.subjectDn = b.subjectDn;
+ this.issuerDn = b.issuerDn;
+ this.created = b.created;
+ this.lastModified = b.lastModified;
+ this.pemEncodedCertificate = b.pemEncodedCertificate;
+ }
+ public String getDisplay() {
return display;
}
- public String getValue() {
-
- return value;
+ public Boolean getPrimary() {
+ return primary;
}
- public Boolean isPrimary() {
-
- return primary;
+ public String getSubjectDn() {
+ return subjectDn;
}
- private ScimX509Certificate(Builder b) {
+ public String getPemEncodedCertificate() {
+ return pemEncodedCertificate;
+ }
- this.display = b.display;
- this.value = b.value;
- this.primary = b.primary;
+ public String getIssuerDn() {
+ return issuerDn;
}
- public static Builder builder() {
+ public Date getCreated() {
+ return created;
+ }
- return new Builder();
+ public Date getLastModified() {
+ return lastModified;
}
+
public static class Builder {
private String display;
- private String value;
+
+ private String subjectDn;
+
+ private String issuerDn;
+
private Boolean primary;
- public Builder() {
+ private String pemEncodedCertificate;
- }
+ private Date created;
- public Builder display(String display) {
+ private Date lastModified;
+ public Builder display(String display) {
this.display = display;
return this;
}
- public Builder value(String value) {
+ public Builder primary(Boolean primary) {
- this.value = value;
+ this.primary = primary;
return this;
}
- public Builder primary(Boolean primary) {
+ public Builder subjectDn(String subjectDn) {
+ this.subjectDn = subjectDn;
+ return this;
+ }
- this.primary = primary;
+ public Builder issuerDn(String issuerDn) {
+ this.issuerDn = issuerDn;
return this;
}
- public ScimX509Certificate build() {
+ public Builder created(Date created) {
+ this.created = created;
+ return this;
+ }
+ public Builder lastModified(Date lastModified) {
+ this.lastModified = lastModified;
+ return this;
+ }
+
+ public Builder pemEncodedCertificate(String certificate) {
+ this.pemEncodedCertificate = certificate;
+ return this;
+ }
+
+ public ScimX509Certificate build() {
return new ScimX509Certificate(this);
}
}
+
+ public static Builder builder() {
+ return new Builder();
+ }
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/provisioning/ScimGroupProvisioning.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/provisioning/ScimGroupProvisioning.java
index b733d605f..db0e1d6d5 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/provisioning/ScimGroupProvisioning.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/provisioning/ScimGroupProvisioning.java
@@ -1,5 +1,7 @@
package it.infn.mw.iam.api.scim.provisioning;
+import static java.lang.String.format;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
@@ -14,6 +16,8 @@
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
+import com.google.common.base.Strings;
+
import it.infn.mw.iam.api.scim.converter.GroupConverter;
import it.infn.mw.iam.api.scim.exception.IllegalArgumentException;
import it.infn.mw.iam.api.scim.exception.ScimException;
@@ -31,7 +35,6 @@
import it.infn.mw.iam.audit.events.group.GroupCreatedEvent;
import it.infn.mw.iam.audit.events.group.GroupRemovedEvent;
import it.infn.mw.iam.audit.events.group.GroupReplacedEvent;
-import it.infn.mw.iam.audit.events.group.GroupUpdatedEvent;
import it.infn.mw.iam.persistence.model.IamAccount;
import it.infn.mw.iam.persistence.model.IamGroup;
import it.infn.mw.iam.persistence.repository.IamAccountRepository;
@@ -41,6 +44,9 @@
public class ScimGroupProvisioning
implements ScimProvisioning>, ApplicationEventPublisherAware {
+ private static final int GROUP_NAME_MAX_LENGTH = 50;
+ private static final int GROUP_FULLNAME_MAX_LENGTH = 512;
+
private final IamGroupRepository groupRepository;
private final IamAccountRepository accountRepository;
@@ -91,6 +97,8 @@ public ScimGroup getById(String id) {
@Override
public ScimGroup create(ScimGroup group) {
+ displayNameSanityChecks(group.getDisplayName());
+
IamGroup iamGroup = new IamGroup();
Date creationTime = new Date();
@@ -100,35 +108,35 @@ public ScimGroup create(ScimGroup group) {
iamGroup.setName(group.getDisplayName());
iamGroup.setCreationTime(creationTime);
iamGroup.setLastUpdateTime(creationTime);
- iamGroup.setAccounts(new HashSet());
+ iamGroup.setAccounts(new HashSet<>());
iamGroup.setChildrenGroups(new HashSet<>());
- if (groupRepository.findByName(group.getDisplayName()).isPresent()) {
- throw new ScimResourceExistsException("Duplicated group '" + group.getDisplayName() + "'");
- }
-
IamGroup iamParentGroup = null;
if (group.getIndigoGroup().getParentGroup() != null) {
String parentGroupUuid = group.getIndigoGroup().getParentGroup().getValue();
+ String parentGroupName = group.getIndigoGroup().getParentGroup().getDisplay();
iamParentGroup = groupRepository.findByUuid(parentGroupUuid)
.orElseThrow(() -> new ScimResourceNotFoundException(
String.format("Parent group '%s' not found", parentGroupUuid)));
+ String fullName = String.format("%s/%s", parentGroupName, group.getDisplayName());
+ fullNameSanityChecks(fullName);
+
iamGroup.setParentGroup(iamParentGroup);
+ iamGroup.setName(fullName);
Set children = iamParentGroup.getChildrenGroups();
children.add(iamGroup);
}
groupRepository.save(iamGroup);
+
if (iamParentGroup != null) {
groupRepository.save(iamParentGroup);
- eventPublisher.publishEvent(
- new GroupCreatedEvent(this, iamGroup, "Group created with name " + iamParentGroup.getName()));
}
-
+
eventPublisher.publishEvent(
new GroupCreatedEvent(this, iamGroup, "Group created with name " + iamGroup.getName()));
@@ -169,9 +177,10 @@ public ScimGroup replace(String id, ScimGroup scimItemToBeReplaced) {
/* displayname is required */
String displayName = scimItemToBeReplaced.getDisplayName();
+ displayNameSanityChecks(displayName);
if (!isGroupNameAvailable(displayName, id)) {
- throw new ScimResourceExistsException(displayName + " is already mappped to another group");
+ throw new ScimResourceExistsException(displayName + " is already mapped to another group");
}
IamGroup updatedGroup = converter.fromScim(scimItemToBeReplaced);
@@ -235,6 +244,7 @@ private void executePatchOperation(IamGroup group, ScimPatchOperation updaters = groupUpdaterFactory.getUpdatersForPatchOperation(group, op);
+ List updatesToPublish = new ArrayList<>();
boolean hasChanged = false;
@@ -244,9 +254,7 @@ private void executePatchOperation(IamGroup group, ScimPatchOperation> op) {
if (op.getPath() == null || op.getPath().isEmpty()) {
throw new ScimPatchOperationNotSupported("empty path value is not currently supported");
}
- if (op.getPath().equals("members")) {
+ if ("members".equals(op.getPath())) {
return;
}
throw new ScimPatchOperationNotSupported(
"path value " + op.getPath() + " is not currently supported");
}
+ private void displayNameSanityChecks(String displayName) {
+ if (Strings.isNullOrEmpty(displayName)) {
+ throw new IllegalArgumentException("Group displayName cannot be empty");
+ }
+
+ if (displayName.contains("/")) {
+ throw new IllegalArgumentException("Group displayName cannot contain a slash character");
+ }
+
+ if (displayName.length() > GROUP_NAME_MAX_LENGTH) {
+ throw new IllegalArgumentException(
+ format("Group name length cannot be higher than %d characters", GROUP_NAME_MAX_LENGTH));
+ }
+ }
+
+ private void fullNameSanityChecks(String displayName) {
+ if (displayName.length() > GROUP_FULLNAME_MAX_LENGTH) {
+ throw new IllegalArgumentException(
+ format("Group displayName length cannot be higher than %d characters",
+ GROUP_FULLNAME_MAX_LENGTH));
+ }
+
+ if (groupRepository.findByName(displayName).isPresent()) {
+ throw new ScimResourceExistsException(format("Duplicated group '%s'", displayName));
+ }
+ }
+
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/provisioning/ScimQuery.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/provisioning/ScimQuery.java
index e6acfa575..dee9c9388 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/provisioning/ScimQuery.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/provisioning/ScimQuery.java
@@ -4,7 +4,7 @@
public interface ScimQuery extends ScimPageRequest {
- public static enum SortOrder {
+ public enum SortOrder {
ascending, descending;
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/provisioning/ScimUserProvisioning.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/provisioning/ScimUserProvisioning.java
index 92ace1b0a..1693ef720 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/provisioning/ScimUserProvisioning.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/provisioning/ScimUserProvisioning.java
@@ -19,10 +19,8 @@
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Date;
import java.util.EnumSet;
import java.util.List;
-import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
@@ -31,15 +29,12 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
-import com.google.common.base.Preconditions;
-
import it.infn.mw.iam.api.scim.converter.OidcIdConverter;
import it.infn.mw.iam.api.scim.converter.SamlIdConverter;
import it.infn.mw.iam.api.scim.converter.SshKeyConverter;
import it.infn.mw.iam.api.scim.converter.UserConverter;
import it.infn.mw.iam.api.scim.converter.X509CertificateConverter;
import it.infn.mw.iam.api.scim.exception.IllegalArgumentException;
-import it.infn.mw.iam.api.scim.exception.ScimException;
import it.infn.mw.iam.api.scim.exception.ScimPatchOperationNotSupported;
import it.infn.mw.iam.api.scim.exception.ScimResourceExistsException;
import it.infn.mw.iam.api.scim.exception.ScimResourceNotFoundException;
@@ -51,45 +46,40 @@
import it.infn.mw.iam.api.scim.updater.AccountUpdater;
import it.infn.mw.iam.api.scim.updater.UpdaterType;
import it.infn.mw.iam.api.scim.updater.factory.DefaultAccountUpdaterFactory;
-import it.infn.mw.iam.audit.events.account.AccountCreatedEvent;
-import it.infn.mw.iam.audit.events.account.AccountRemovedEvent;
import it.infn.mw.iam.audit.events.account.AccountReplacedEvent;
-import it.infn.mw.iam.audit.events.account.AccountUpdatedEvent;
+import it.infn.mw.iam.core.user.IamAccountService;
+import it.infn.mw.iam.core.user.exception.CredentialAlreadyBoundException;
+import it.infn.mw.iam.core.user.exception.UserAlreadyExistsException;
import it.infn.mw.iam.persistence.model.IamAccount;
-import it.infn.mw.iam.persistence.model.IamOidcId;
-import it.infn.mw.iam.persistence.model.IamSamlId;
-import it.infn.mw.iam.persistence.model.IamSshKey;
-import it.infn.mw.iam.persistence.model.IamX509Certificate;
import it.infn.mw.iam.persistence.repository.IamAccountRepository;
-import it.infn.mw.iam.persistence.repository.IamAuthoritiesRepository;
@Service
public class ScimUserProvisioning
implements ScimProvisioning, ApplicationEventPublisherAware {
- public static final EnumSet SUPPORTED_UPDATER_TYPES = EnumSet.of(ACCOUNT_ADD_OIDC_ID,
+ protected static final EnumSet SUPPORTED_UPDATER_TYPES = EnumSet.of(ACCOUNT_ADD_OIDC_ID,
ACCOUNT_REMOVE_OIDC_ID, ACCOUNT_ADD_SAML_ID, ACCOUNT_REMOVE_SAML_ID, ACCOUNT_ADD_SSH_KEY,
ACCOUNT_REMOVE_SSH_KEY, ACCOUNT_ADD_X509_CERTIFICATE, ACCOUNT_REMOVE_X509_CERTIFICATE,
ACCOUNT_REPLACE_ACTIVE, ACCOUNT_REPLACE_EMAIL, ACCOUNT_REPLACE_FAMILY_NAME,
ACCOUNT_REPLACE_GIVEN_NAME, ACCOUNT_REPLACE_PASSWORD, ACCOUNT_REPLACE_PICTURE,
ACCOUNT_REPLACE_USERNAME, ACCOUNT_REMOVE_PICTURE);
+ private final IamAccountService accountService;
private final IamAccountRepository accountRepository;
- private final IamAuthoritiesRepository authorityRepository;
+
private final DefaultAccountUpdaterFactory updatersFactory;
- private final PasswordEncoder passwordEncoder;
+
private final UserConverter userConverter;
private ApplicationEventPublisher eventPublisher;
@Autowired
- public ScimUserProvisioning(IamAccountRepository accountRepository,
- IamAuthoritiesRepository authorityRepository, PasswordEncoder passwordEncoder,
+ public ScimUserProvisioning(IamAccountService accountService,
+ IamAccountRepository accountRepository, PasswordEncoder passwordEncoder,
UserConverter userConverter, OidcIdConverter oidcIdConverter, SamlIdConverter samlIdConverter,
SshKeyConverter sshKeyConverter, X509CertificateConverter x509CertificateConverter) {
+ this.accountService = accountService;
this.accountRepository = accountRepository;
- this.authorityRepository = authorityRepository;
- this.passwordEncoder = passwordEncoder;
this.userConverter = userConverter;
this.updatersFactory = new DefaultAccountUpdaterFactory(passwordEncoder, accountRepository,
oidcIdConverter, samlIdConverter, sshKeyConverter, x509CertificateConverter);
@@ -130,165 +120,24 @@ public void delete(final String id) {
IamAccount account = accountRepository.findByUuid(id)
.orElseThrow(() -> new ScimResourceNotFoundException("No user mapped to id '" + id + "'"));
- accountRepository.delete(account);
-
- eventPublisher.publishEvent(
- new AccountRemovedEvent(this, account, "Removed account for user " + account.getUsername()));
- }
-
- private void checkForDuplicates(ScimUser user) throws ScimResourceExistsException {
-
- Preconditions.checkNotNull(user.getEmails());
- Preconditions.checkNotNull(user.getEmails().get(0));
- Preconditions.checkNotNull(user.getEmails().get(0).getValue());
-
- accountRepository.findByUsername(user.getUserName()).ifPresent(a -> {
- throw new ScimResourceExistsException("userName is already taken: " + a.getUsername());
- });
-
- accountRepository.findByEmail(user.getEmails().get(0).getValue()).ifPresent(a -> {
- throw new ScimResourceExistsException(
- "email already assigned to an existing user: " + a.getUserInfo().getEmail());
- });
+ accountService.deleteAccount(account);
+
}
- public IamAccount createAccount(final ScimUser user) {
-
- checkForDuplicates(user);
-
- final Date creationTime = new Date();
- final String uuid = UUID.randomUUID().toString();
-
- IamAccount account = userConverter.fromScim(user);
- account.setUuid(uuid);
- account.setCreationTime(creationTime);
- account.setLastUpdateTime(creationTime);
- account.setUsername(user.getUserName());
-
- if (user.getActive() != null) {
- account.setActive(user.getActive());
- } else {
- /* if no active status is specified, disable user */
- account.setActive(false);
- }
-
- /* users created via SCIM are set with email-verified as true */
- account.getUserInfo().setEmailVerified(true);
-
- if (account.getPassword() == null) {
- account.setPassword(UUID.randomUUID().toString());
- }
-
- account.setPassword(passwordEncoder.encode(account.getPassword()));
-
- authorityRepository.findByAuthority("ROLE_USER")
- .map(a -> account.getAuthorities().add(a))
- .orElseThrow(
- () -> new IllegalStateException("ROLE_USER not found in database. This is a bug"));
-
- if (account.hasX509Certificates()) {
-
- account.getX509Certificates().forEach(cert -> checkX509CertificateNotExists(cert));
-
- long count = account.getX509Certificates().stream().filter(cert -> cert.isPrimary()).count();
-
- if (count > 1) {
-
- throw new ScimException("Too many primary x509 certificates provided!");
- }
-
- if (count == 0) {
-
- account.getX509Certificates().stream().findFirst().get().setPrimary(true);
- }
- }
-
- if (account.hasOidcIds()) {
-
- account.getOidcIds().forEach(oidcId -> checkOidcIdNotAlreadyBounded(oidcId));
- }
-
- if (account.hasSshKeys()) {
-
- account.getSshKeys().forEach(sshKey -> checkSshKeyNotExists(sshKey));
-
- long count = account.getSshKeys().stream().filter(sshKey -> sshKey.isPrimary()).count();
-
- if (count > 1) {
-
- throw new ScimException("Too many primary ssh keys provided!");
- }
-
- if (count == 0) {
-
- account.getSshKeys().stream().findFirst().get().setPrimary(true);
- }
- }
-
- if (account.hasSamlIds()) {
-
- account.getSamlIds().forEach(samlId -> checkSamlIdNotAlreadyBounded(samlId));
- }
-
- accountRepository.save(account);
-
- eventPublisher.publishEvent(
- new AccountCreatedEvent(this, account, "Account created for user " + account.getUsername()));
-
- return account;
- }
@Override
public ScimUser create(final ScimUser user) {
- IamAccount account = createAccount(user);
-
- return userConverter.toScim(account);
- }
+ IamAccount newAccount = userConverter.fromScim(user);
- private void checkX509CertificateNotExists(IamX509Certificate cert) {
-
- if (accountRepository.findByCertificate(cert.getCertificate()).isPresent()) {
-
- throw new ScimResourceExistsException(
- String.format("X509 Certificate %s is already mapped to a user", cert.getCertificate()));
+ try {
+ IamAccount account = accountService.createAccount(newAccount);
+ return userConverter.toScim(account);
+ } catch (CredentialAlreadyBoundException | UserAlreadyExistsException e) {
+ throw new ScimResourceExistsException(e.getMessage(),e);
}
}
- private void checkOidcIdNotAlreadyBounded(IamOidcId oidcId) {
-
- Preconditions.checkNotNull(oidcId);
- Preconditions.checkNotNull(oidcId.getIssuer());
- Preconditions.checkNotNull(oidcId.getSubject());
- accountRepository.findByOidcId(oidcId.getIssuer(), oidcId.getSubject()).ifPresent(account -> {
- throw new ScimResourceExistsException(
- String.format("OIDC id (%s,%s) already bounded to another user", oidcId.getIssuer(),
- oidcId.getSubject()));
- });
- }
-
- private void checkSshKeyNotExists(IamSshKey sshKey) {
-
- Preconditions.checkNotNull(sshKey);
- Preconditions.checkNotNull(sshKey.getValue());
- accountRepository.findBySshKeyValue(sshKey.getValue()).ifPresent(account -> {
- throw new ScimResourceExistsException(
- String.format("Ssh key (%s) already bounded to another user", sshKey.getValue()));
- });
- }
-
- private void checkSamlIdNotAlreadyBounded(IamSamlId samlId) {
-
- Preconditions.checkNotNull(samlId);
- Preconditions.checkNotNull(samlId.getIdpId());
- Preconditions.checkNotNull(samlId.getUserId());
- accountRepository.findBySamlId(samlId.getIdpId(), samlId.getUserId()).ifPresent(account -> {
- throw new ScimResourceExistsException(
- String.format("SAML id (%s,%s) already bounded to another user", samlId.getIdpId(),
- samlId.getUserId()));
- });
- }
-
@Override
public ScimListResponse list(final ScimPageRequest params) {
@@ -342,10 +191,6 @@ public ScimUser replace(final String uuid, final ScimUser scimItemToBeUpdated) {
updatedAccount.setActive(existingAccount.isActive());
}
- if (scimItemToBeUpdated.getPassword() != null) {
-
- }
-
updatedAccount.touch();
accountRepository.save(updatedAccount);
@@ -360,23 +205,31 @@ public ScimUser replace(final String uuid, final ScimUser scimItemToBeUpdated) {
private void executePatchOperation(IamAccount account, ScimPatchOperation op) {
List updaters = updatersFactory.getUpdatersForPatchOperation(account, op);
+ List updatesToPublish = new ArrayList<>();
- boolean hasChanged = false;
+ boolean oneUpdaterChangedAccount = false;
for (AccountUpdater u : updaters) {
if (!SUPPORTED_UPDATER_TYPES.contains(u.getType())) {
throw new ScimPatchOperationNotSupported(u.getType().getDescription() + " not supported");
}
- hasChanged |= u.update();
- eventPublisher.publishEvent(new AccountUpdatedEvent(this, account, u.getType(),
- String.format("Updated account information for user %s", account.getUsername())));
+ boolean lastUpdaterChangedAccount = u.update();
+
+ oneUpdaterChangedAccount |= lastUpdaterChangedAccount;
+
+ if (lastUpdaterChangedAccount) {
+ updatesToPublish.add(u);
+ }
}
- if (hasChanged) {
+ if (oneUpdaterChangedAccount) {
account.touch();
accountRepository.save(account);
+ for (AccountUpdater u : updatesToPublish) {
+ u.publishUpdateEvent(this, eventPublisher);
+ }
}
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/AccountEventBuilder.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/AccountEventBuilder.java
new file mode 100644
index 000000000..f93247c1f
--- /dev/null
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/AccountEventBuilder.java
@@ -0,0 +1,11 @@
+package it.infn.mw.iam.api.scim.updater;
+
+import it.infn.mw.iam.audit.events.account.AccountEvent;
+import it.infn.mw.iam.persistence.model.IamAccount;
+
+@FunctionalInterface
+public interface AccountEventBuilder {
+
+ E buildEvent(Object source, IamAccount account, T newValue);
+
+}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/AccountEventPublisher.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/AccountEventPublisher.java
new file mode 100644
index 000000000..04a85afba
--- /dev/null
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/AccountEventPublisher.java
@@ -0,0 +1,9 @@
+package it.infn.mw.iam.api.scim.updater;
+
+import org.springframework.context.ApplicationEventPublisher;
+
+@FunctionalInterface
+public interface AccountEventPublisher {
+
+ void publishAccountEvent(Object source, ApplicationEventPublisher publisher);
+}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/AccountUpdaterFactory.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/AccountUpdaterFactory.java
index 620402f73..8ede9c9e1 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/AccountUpdaterFactory.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/AccountUpdaterFactory.java
@@ -7,12 +7,13 @@
/**
* Builds a list of {@link AccountUpdater} objects linked to a patch operation
*/
-public interface AccountUpdaterFactory {
+@FunctionalInterface
+public interface AccountUpdaterFactory {
/**
*
* @param entity @param u @return
*/
- List getUpdatersForPatchOperation(EntityType entity, ScimPatchOperation u);
+ List getUpdatersForPatchOperation(E entity, ScimPatchOperation u);
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/DefaultAccountUpdater.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/DefaultAccountUpdater.java
index 47ca10375..1e53034fb 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/DefaultAccountUpdater.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/DefaultAccountUpdater.java
@@ -4,22 +4,29 @@
import java.util.function.Predicate;
import java.util.function.Supplier;
+import org.springframework.context.ApplicationEventPublisher;
+
+import it.infn.mw.iam.audit.events.account.AccountEvent;
import it.infn.mw.iam.persistence.model.IamAccount;
-public class DefaultAccountUpdater extends DefaultUpdater implements AccountUpdater {
+public class DefaultAccountUpdater extends DefaultUpdater
+ implements AccountUpdater {
private final IamAccount account;
-
- public DefaultAccountUpdater(IamAccount account, UpdaterType type, Supplier supplier, Consumer consumer,
- T newVal) {
+ private final AccountEventBuilder eventBuilder;
+
+ public DefaultAccountUpdater(IamAccount account, UpdaterType type, Supplier supplier,
+ Consumer consumer, T newVal, AccountEventBuilder eventBuilder) {
super(type, supplier, consumer, newVal);
this.account = account;
+ this.eventBuilder = eventBuilder;
}
public DefaultAccountUpdater(IamAccount account, UpdaterType type, Consumer consumer, T newVal,
- Predicate predicate) {
+ Predicate predicate, AccountEventBuilder eventBuilder) {
super(type, consumer, newVal, predicate);
this.account = account;
+ this.eventBuilder = eventBuilder;
}
@Override
@@ -27,4 +34,12 @@ public IamAccount getAccount() {
return this.account;
}
+ @Override
+ public void publishUpdateEvent(Object source, ApplicationEventPublisher publisher) {
+
+ if (eventBuilder != null) {
+ publisher.publishEvent(eventBuilder.buildEvent(source, account, newValue));
+ }
+ }
+
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/DefaultUpdater.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/DefaultUpdater.java
index 646b02897..c009622e3 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/DefaultUpdater.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/DefaultUpdater.java
@@ -9,7 +9,7 @@
import it.infn.mw.iam.api.scim.updater.util.NullSafeNotEqualsMatcher;
-public class DefaultUpdater implements Updater {
+public abstract class DefaultUpdater implements Updater {
public static final Logger LOG = LoggerFactory.getLogger(DefaultUpdater.class);
@@ -48,7 +48,7 @@ public boolean update() {
}
private static NullSafeNotEqualsMatcher nullSafeNotEqualsMatcher(Supplier supp) {
- return new NullSafeNotEqualsMatcher(supp);
+ return new NullSafeNotEqualsMatcher<>(supp);
}
@Override
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/Updater.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/Updater.java
index 58de85fce..3e029755a 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/Updater.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/Updater.java
@@ -1,5 +1,7 @@
package it.infn.mw.iam.api.scim.updater;
+import org.springframework.context.ApplicationEventPublisher;
+
/**
* And updater attempts to update something, and returns true if that something was actually updated
*
@@ -9,8 +11,11 @@ public interface Updater {
/**
* The updater update logic
*
- * @return true
, if the object was modified by the update
- * false
, otherwise
+ * @return
+ *
+ * true
, if the object was modified by the update
+ * false
, otherwise
+ *
*/
boolean update();
@@ -20,4 +25,6 @@ public interface Updater {
* @return the updater type (see {@link UpdaterType})
*/
UpdaterType getType();
+
+ void publishUpdateEvent(Object source, ApplicationEventPublisher publisher);
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/builders/AccountUpdaters.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/builders/AccountUpdaters.java
index 7a718a3e9..61bee03e7 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/builders/AccountUpdaters.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/builders/AccountUpdaters.java
@@ -7,6 +7,9 @@
public class AccountUpdaters {
+ private AccountUpdaters() {
+ }
+
public static Adders adders(IamAccountRepository repo, PasswordEncoder encoder,
IamAccount account) {
return new Adders(repo, encoder, account);
@@ -20,5 +23,7 @@ public static Replacers replacers(IamAccountRepository repo, PasswordEncoder enc
IamAccount account) {
return new Replacers(repo, encoder, account);
}
+
+
}
\ No newline at end of file
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/builders/Adders.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/builders/Adders.java
index 2095f9f2a..f35924cd0 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/builders/Adders.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/builders/Adders.java
@@ -13,11 +13,19 @@
import org.springframework.security.crypto.password.PasswordEncoder;
+import com.google.common.base.Strings;
+
+import it.infn.mw.iam.api.scim.exception.IllegalArgumentException;
import it.infn.mw.iam.api.scim.exception.ScimResourceExistsException;
import it.infn.mw.iam.api.scim.updater.AccountUpdater;
import it.infn.mw.iam.api.scim.updater.DefaultAccountUpdater;
import it.infn.mw.iam.api.scim.updater.util.AccountFinder;
import it.infn.mw.iam.api.scim.updater.util.IdNotBoundChecker;
+import it.infn.mw.iam.audit.events.account.group.GroupMembershipAddedEvent;
+import it.infn.mw.iam.audit.events.account.oidc.OidcAccountAddedEvent;
+import it.infn.mw.iam.audit.events.account.saml.SamlAccountAddedEvent;
+import it.infn.mw.iam.audit.events.account.ssh.SshKeyAddedEvent;
+import it.infn.mw.iam.audit.events.account.x509.X509CertificateAddedEvent;
import it.infn.mw.iam.persistence.model.IamAccount;
import it.infn.mw.iam.persistence.model.IamGroup;
import it.infn.mw.iam.persistence.model.IamOidcId;
@@ -38,144 +46,155 @@ public class Adders extends Replacers {
final AccountFinder findByOidcId;
final AccountFinder findBySamlId;
final AccountFinder findBySshKey;
- final AccountFinder findByX509Certificate;
+ final AccountFinder findByX509CertificateSubject;
+
+ public Adders(IamAccountRepository repo, PasswordEncoder encoder, IamAccount account) {
+ super(repo, encoder, account);
+
+ findByOidcId = id -> repo.findByOidcId(id.getIssuer(), id.getSubject());
+ findBySamlId = repo::findBySamlId;
+ findBySshKey = key -> repo.findBySshKeyValue(key.getValue());
+ findByX509CertificateSubject = cert -> repo.findByCertificateSubject(cert.getSubjectDn());
+
+ oidcIdAddChecks = buildOidcIdsAddChecks();
+ samlIdAddChecks = buildSamlIdsAddChecks();
+ sshKeyAddChecks = buildSshKeyAddChecks();
+ x509CertificateAddChecks = buildX509CertificateAddChecks();
+ addMembersChecks = buildAddMembersCheck();
+ }
+
private Predicate> buildOidcIdsAddChecks() {
Predicate oidcIdNotBound =
- new IdNotBoundChecker(findByOidcId, account, (id, a) -> {
+ new IdNotBoundChecker<>(findByOidcId, account, (id, a) -> {
throw new ScimResourceExistsException(
"OpenID connect account " + id + " already bound to another user");
});
Predicate> oidcIdsNotBound = c -> {
c.removeIf(Objects::isNull);
- c.stream().forEach(id -> oidcIdNotBound.test(id));
+ c.stream().forEach(oidcIdNotBound::test);
return true;
};
- Predicate> oidcIdsNotOwned = c -> {
- return !account.getOidcIds().containsAll(c);
- };
+ Predicate> oidcIdsNotOwned = c -> !account.getOidcIds().containsAll(c);
return oidcIdsNotBound.and(oidcIdsNotOwned);
}
private Predicate> buildSamlIdsAddChecks() {
+
+ Predicate> samlIdWellFormed = c -> {
+ c.removeIf(Objects::isNull);
+ c.stream().forEach(id -> {
+ if (Strings.isNullOrEmpty(id.getIdpId())) {
+ throw new IllegalArgumentException("idpId cannot be null or empty!");
+ }
+
+ if (Strings.isNullOrEmpty(id.getAttributeId())) {
+ throw new IllegalArgumentException("attributeId cannot be null or empty!");
+ }
+
+ if (Strings.isNullOrEmpty(id.getUserId())) {
+ throw new IllegalArgumentException("userId cannot be null or empty!");
+ }
+ });
+
+ return true;
+ };
+
Predicate samlIdNotBound =
- new IdNotBoundChecker(findBySamlId, account, (id, a) -> {
+ new IdNotBoundChecker<>(findBySamlId, account, (id, a) -> {
throw new ScimResourceExistsException(
"SAML account " + id + " already bound to another user");
});
Predicate> samlIdsNotBound = c -> {
- c.removeIf(Objects::isNull);
- c.stream().forEach(id -> samlIdNotBound.test(id));
+ c.stream().forEach(samlIdNotBound::test);
return true;
};
- Predicate> samlIdsNotOwned = c -> {
- return !account.getSamlIds().containsAll(c);
- };
+ Predicate> samlIdsNotOwned = c -> !account.getSamlIds().containsAll(c);
- return samlIdsNotBound.and(samlIdsNotOwned);
+ return samlIdWellFormed.and(samlIdsNotBound.and(samlIdsNotOwned));
}
private Predicate> buildSshKeyAddChecks() {
Predicate sshKeyNotBound =
- new IdNotBoundChecker(findBySshKey, account, (key, a) -> {
+ new IdNotBoundChecker<>(findBySshKey, account, (key, a) -> {
throw new ScimResourceExistsException(
"SSH key '" + key.getValue() + "' already bound to another user");
});
Predicate> sshKeysNotBound = c -> {
c.removeIf(Objects::isNull);
- c.stream().forEach(id -> sshKeyNotBound.test(id));
+ c.stream().forEach(sshKeyNotBound::test);
return true;
};
- Predicate> sshKeysNotOwned = c -> {
- return !account.getSshKeys().containsAll(c);
- };
-
+ Predicate> sshKeysNotOwned = c -> !account.getSshKeys().containsAll(c);
return sshKeysNotBound.and(sshKeysNotOwned);
}
private Predicate> buildX509CertificateAddChecks() {
Predicate x509CertificateNotBound =
- new IdNotBoundChecker(findByX509Certificate, account, (cert, a) -> {
- throw new ScimResourceExistsException(
- "X509 Certificate " + cert.getCertificate() + "' already bound to another user");
+ new IdNotBoundChecker<>(findByX509CertificateSubject, account, (cert, a) -> {
+ throw new ScimResourceExistsException("X509 certificate with subject '"
+ + cert.getSubjectDn() + "' is already bound to another user");
});
Predicate> x509CertificatesNotBound = c -> {
c.removeIf(Objects::isNull);
- c.stream().forEach(id -> x509CertificateNotBound.test(id));
+ c.stream().forEach(x509CertificateNotBound::test);
return true;
};
- Predicate> x509CertificatesNotOwned = c -> {
- return !account.getX509Certificates().containsAll(c);
- };
-
+ Predicate> x509CertificatesNotOwned =
+ c -> !account.getX509Certificates().containsAll(c);
return x509CertificatesNotBound.and(x509CertificatesNotOwned);
}
private Predicate> buildAddMembersCheck() {
-
- Predicate> notAlreadyMember = a -> {
- return !account.getGroups().containsAll(a);
- };
-
- return notAlreadyMember;
- }
-
- public Adders(IamAccountRepository repo, PasswordEncoder encoder, IamAccount account) {
- super(repo, encoder, account);
-
- findByOidcId = id -> repo.findByOidcId(id.getIssuer(), id.getSubject());
- findBySamlId = id -> repo.findBySamlId(id.getIdpId(), id.getUserId());
- findBySshKey = key -> repo.findBySshKeyValue(key.getValue());
- findByX509Certificate = cert -> repo.findByCertificate(cert.getCertificate());
-
- oidcIdAddChecks = buildOidcIdsAddChecks();
- samlIdAddChecks = buildSamlIdsAddChecks();
- sshKeyAddChecks = buildSshKeyAddChecks();
- x509CertificateAddChecks = buildX509CertificateAddChecks();
- addMembersChecks = buildAddMembersCheck();
+ return a -> !account.getGroups().containsAll(a);
}
public AccountUpdater oidcId(Collection newOidcIds) {
- return new DefaultAccountUpdater>(account, ACCOUNT_ADD_OIDC_ID,
- account::linkOidcIds, newOidcIds, oidcIdAddChecks);
+ return new DefaultAccountUpdater, OidcAccountAddedEvent>(account,
+ ACCOUNT_ADD_OIDC_ID, account::linkOidcIds, newOidcIds, oidcIdAddChecks,
+ OidcAccountAddedEvent::new);
}
public AccountUpdater samlId(Collection newSamlIds) {
- return new DefaultAccountUpdater>(account, ACCOUNT_ADD_SAML_ID,
- account::linkSamlIds, newSamlIds, samlIdAddChecks);
+ return new DefaultAccountUpdater, SamlAccountAddedEvent>(account,
+ ACCOUNT_ADD_SAML_ID, account::linkSamlIds, newSamlIds, samlIdAddChecks,
+ SamlAccountAddedEvent::new);
}
public AccountUpdater sshKey(Collection newSshKeys) {
- return new DefaultAccountUpdater>(account, ACCOUNT_ADD_SSH_KEY,
- account::linkSshKeys, newSshKeys, sshKeyAddChecks);
+ return new DefaultAccountUpdater, SshKeyAddedEvent>(account,
+ ACCOUNT_ADD_SSH_KEY, account::linkSshKeys, newSshKeys, sshKeyAddChecks,
+ SshKeyAddedEvent::new);
}
public AccountUpdater x509Certificate(Collection newX509Certificates) {
- return new DefaultAccountUpdater>(account, ACCOUNT_ADD_X509_CERTIFICATE,
- account::linkX509Certificates, newX509Certificates, x509CertificateAddChecks);
+ return new DefaultAccountUpdater, X509CertificateAddedEvent>(
+ account, ACCOUNT_ADD_X509_CERTIFICATE, account::linkX509Certificates, newX509Certificates,
+ x509CertificateAddChecks, X509CertificateAddedEvent::new);
}
public AccountUpdater group(Collection groups) {
- return new DefaultAccountUpdater>(account, ACCOUNT_ADD_GROUP_MEMBERSHIP,
- account::linkMembers, groups, addMembersChecks);
+ return new DefaultAccountUpdater, GroupMembershipAddedEvent>(account,
+ ACCOUNT_ADD_GROUP_MEMBERSHIP, account::linkMembers, groups, addMembersChecks,
+ GroupMembershipAddedEvent::new);
}
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/builders/Removers.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/builders/Removers.java
index 77d230a03..ac64246c1 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/builders/Removers.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/builders/Removers.java
@@ -12,6 +12,12 @@
import it.infn.mw.iam.api.scim.updater.AccountUpdater;
import it.infn.mw.iam.api.scim.updater.DefaultAccountUpdater;
+import it.infn.mw.iam.audit.events.account.PictureRemovedEvent;
+import it.infn.mw.iam.audit.events.account.group.GroupMembershipRemovedEvent;
+import it.infn.mw.iam.audit.events.account.oidc.OidcAccountRemovedEvent;
+import it.infn.mw.iam.audit.events.account.saml.SamlAccountRemovedEvent;
+import it.infn.mw.iam.audit.events.account.ssh.SshKeyRemovedEvent;
+import it.infn.mw.iam.audit.events.account.x509.X509CertificateRemovedEvent;
import it.infn.mw.iam.persistence.model.IamAccount;
import it.infn.mw.iam.persistence.model.IamGroup;
import it.infn.mw.iam.persistence.model.IamOidcId;
@@ -22,44 +28,50 @@
import it.infn.mw.iam.persistence.repository.IamAccountRepository;
public class Removers extends AccountBuilderSupport {
-
+
public Removers(IamAccountRepository repo, IamAccount account) {
super(repo, account);
}
public AccountUpdater oidcId(Collection toBeRemoved) {
- return new DefaultAccountUpdater>(account, ACCOUNT_REMOVE_OIDC_ID, account::unlinkOidcIds,
- toBeRemoved, i -> !Collections.disjoint(account.getOidcIds(), i));
+ return new DefaultAccountUpdater, OidcAccountRemovedEvent>(account,
+ ACCOUNT_REMOVE_OIDC_ID, account::unlinkOidcIds, toBeRemoved,
+ i -> !Collections.disjoint(account.getOidcIds(), i), OidcAccountRemovedEvent::new);
}
public AccountUpdater samlId(Collection toBeRemoved) {
- return new DefaultAccountUpdater>(account, ACCOUNT_REMOVE_SAML_ID, account::unlinkSamlIds,
- toBeRemoved, i -> !Collections.disjoint(account.getSamlIds(), i));
+ return new DefaultAccountUpdater, SamlAccountRemovedEvent>(account,
+ ACCOUNT_REMOVE_SAML_ID, account::unlinkSamlIds, toBeRemoved,
+ i -> !Collections.disjoint(account.getSamlIds(), i), SamlAccountRemovedEvent::new);
}
public AccountUpdater sshKey(Collection toBeRemoved) {
- return new DefaultAccountUpdater>(account, ACCOUNT_REMOVE_SSH_KEY, account::unlinkSshKeys,
- toBeRemoved, i -> !Collections.disjoint(account.getSshKeys(), i));
+ return new DefaultAccountUpdater, SshKeyRemovedEvent>(account,
+ ACCOUNT_REMOVE_SSH_KEY, account::unlinkSshKeys, toBeRemoved,
+ i -> !Collections.disjoint(account.getSshKeys(), i), SshKeyRemovedEvent::new);
}
public AccountUpdater x509Certificate(Collection toBeRemoved) {
- return new DefaultAccountUpdater>(account, ACCOUNT_REMOVE_X509_CERTIFICATE,
- account::unlinkX509Certificates, toBeRemoved,
- i -> !Collections.disjoint(account.getX509Certificates(), i));
+ return new DefaultAccountUpdater, X509CertificateRemovedEvent>(
+ account, ACCOUNT_REMOVE_X509_CERTIFICATE, account::unlinkX509Certificates, toBeRemoved,
+ i -> !Collections.disjoint(account.getX509Certificates(), i),
+ X509CertificateRemovedEvent::new);
}
public AccountUpdater group(Collection toBeRemoved) {
- return new DefaultAccountUpdater>(account, ACCOUNT_REMOVE_GROUP_MEMBERSHIP, account::unlinkMembers,
- toBeRemoved, i -> !Collections.disjoint(account.getGroups(), i));
+ return new DefaultAccountUpdater, GroupMembershipRemovedEvent>(account,
+ ACCOUNT_REMOVE_GROUP_MEMBERSHIP, account::unlinkMembers, toBeRemoved,
+ i -> !Collections.disjoint(account.getGroups(), i), GroupMembershipRemovedEvent::new);
}
public AccountUpdater picture(String picture) {
final IamUserInfo ui = account.getUserInfo();
- return new DefaultAccountUpdater(account, ACCOUNT_REMOVE_PICTURE, ui::getPicture, ui::setPicture, null);
+ return new DefaultAccountUpdater(account, ACCOUNT_REMOVE_PICTURE,
+ ui::getPicture, ui::setPicture, null, PictureRemovedEvent::new);
}
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/builders/Replacers.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/builders/Replacers.java
index e1bb570c1..53b6b933b 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/builders/Replacers.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/builders/Replacers.java
@@ -18,6 +18,13 @@
import it.infn.mw.iam.api.scim.updater.DefaultAccountUpdater;
import it.infn.mw.iam.api.scim.updater.util.AccountFinder;
import it.infn.mw.iam.api.scim.updater.util.IdNotBoundChecker;
+import it.infn.mw.iam.audit.events.account.ActiveReplacedEvent;
+import it.infn.mw.iam.audit.events.account.EmailReplacedEvent;
+import it.infn.mw.iam.audit.events.account.FamilyNameReplacedEvent;
+import it.infn.mw.iam.audit.events.account.GivenNameReplacedEvent;
+import it.infn.mw.iam.audit.events.account.PasswordReplacedEvent;
+import it.infn.mw.iam.audit.events.account.PictureReplacedEvent;
+import it.infn.mw.iam.audit.events.account.UsernameReplacedEvent;
import it.infn.mw.iam.persistence.model.IamAccount;
import it.infn.mw.iam.persistence.model.IamUserInfo;
import it.infn.mw.iam.persistence.repository.IamAccountRepository;
@@ -36,8 +43,8 @@ public class Replacers extends AccountBuilderSupport {
public Replacers(IamAccountRepository repo, PasswordEncoder encoder, IamAccount account) {
super(repo, encoder, account);
- findByEmail = e -> repo.findByEmail(e);
- findByUsername = u -> repo.findByUsername(u);
+ findByEmail = repo::findByEmail;
+ findByUsername = repo::findByUsername;
encodedPasswordSetter = t -> account.setPassword(encoder.encode(t));
encodedPasswordChecker = t -> !encoder.matches(t, account.getPassword());
emailAddChecks = buildEmailAddChecks();
@@ -47,7 +54,7 @@ public Replacers(IamAccountRepository repo, PasswordEncoder encoder, IamAccount
private Predicate buildEmailAddChecks() {
Predicate emailNotBound =
- new IdNotBoundChecker(findByEmail, account, (e, a) -> {
+ new IdNotBoundChecker<>(findByEmail, account, (e, a) -> {
throw new ScimResourceExistsException("Email " + e + " already bound to another user");
});
@@ -58,7 +65,7 @@ private Predicate buildEmailAddChecks() {
private Predicate buildUsernameAddChecks() {
Predicate usernameNotBound =
- new IdNotBoundChecker(findByUsername, account, (e, a) -> {
+ new IdNotBoundChecker<>(findByUsername, account, (e, a) -> {
throw new ScimResourceExistsException("Username " + e + " already bound to another user");
});
@@ -70,46 +77,51 @@ private Predicate buildUsernameAddChecks() {
public AccountUpdater givenName(String givenName) {
IamUserInfo ui = account.getUserInfo();
- return new DefaultAccountUpdater(account, ACCOUNT_REPLACE_GIVEN_NAME, ui::getGivenName,
- ui::setGivenName, givenName);
+ return new DefaultAccountUpdater(account,
+ ACCOUNT_REPLACE_GIVEN_NAME, ui::getGivenName, ui::setGivenName, givenName,
+ GivenNameReplacedEvent::new);
}
public AccountUpdater familyName(String familyName) {
final IamUserInfo ui = account.getUserInfo();
- return new DefaultAccountUpdater(account, ACCOUNT_REPLACE_FAMILY_NAME, ui::getFamilyName,
- ui::setFamilyName, familyName);
+ return new DefaultAccountUpdater(account,
+ ACCOUNT_REPLACE_FAMILY_NAME, ui::getFamilyName, ui::setFamilyName, familyName,
+ FamilyNameReplacedEvent::new);
}
public AccountUpdater picture(String newPicture) {
final IamUserInfo ui = account.getUserInfo();
- return new DefaultAccountUpdater(account, ACCOUNT_REPLACE_PICTURE, ui::getPicture, ui::setPicture,
- newPicture);
+ return new DefaultAccountUpdater(account, ACCOUNT_REPLACE_PICTURE,
+ ui::getPicture, ui::setPicture, newPicture, PictureReplacedEvent::new);
}
public AccountUpdater email(String email) {
final IamUserInfo ui = account.getUserInfo();
- return new DefaultAccountUpdater(account, ACCOUNT_REPLACE_EMAIL, ui::setEmail, email, emailAddChecks);
+ return new DefaultAccountUpdater(account, ACCOUNT_REPLACE_EMAIL,
+ ui::setEmail, email, emailAddChecks, EmailReplacedEvent::new);
}
public AccountUpdater password(String newPassword) {
- return new DefaultAccountUpdater(account, ACCOUNT_REPLACE_PASSWORD, encodedPasswordSetter, newPassword,
- encodedPasswordChecker);
+ return new DefaultAccountUpdater(account,
+ ACCOUNT_REPLACE_PASSWORD, encodedPasswordSetter, newPassword, encodedPasswordChecker,
+ PasswordReplacedEvent::new);
}
public AccountUpdater username(String newUsername) {
- return new DefaultAccountUpdater(account, ACCOUNT_REPLACE_USERNAME, account::setUsername, newUsername,
- usernameAddChecks);
+ return new DefaultAccountUpdater(account,
+ ACCOUNT_REPLACE_USERNAME, account::setUsername, newUsername, usernameAddChecks,
+ UsernameReplacedEvent::new);
}
public AccountUpdater active(boolean isActive) {
- return new DefaultAccountUpdater(account, ACCOUNT_REPLACE_ACTIVE, account::isActive,
- account::setActive, isActive);
+ return new DefaultAccountUpdater(account, ACCOUNT_REPLACE_ACTIVE,
+ account::isActive, account::setActive, isActive, ActiveReplacedEvent::new);
}
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/factory/DefaultAccountUpdaterFactory.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/factory/DefaultAccountUpdaterFactory.java
index b442647fe..44a3a4b71 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/factory/DefaultAccountUpdaterFactory.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/factory/DefaultAccountUpdaterFactory.java
@@ -17,6 +17,7 @@
import it.infn.mw.iam.api.scim.converter.SamlIdConverter;
import it.infn.mw.iam.api.scim.converter.SshKeyConverter;
import it.infn.mw.iam.api.scim.converter.X509CertificateConverter;
+import it.infn.mw.iam.api.scim.exception.ScimPatchOperationNotSupported;
import it.infn.mw.iam.api.scim.model.ScimOidcId;
import it.infn.mw.iam.api.scim.model.ScimPatchOperation;
import it.infn.mw.iam.api.scim.model.ScimSamlId;
@@ -64,27 +65,28 @@ public DefaultAccountUpdaterFactory(PasswordEncoder encoder, IamAccountRepositor
private ScimCollectionConverter sshKeyConverter(ScimUser user) {
- return new ScimCollectionConverter(user.getIndigoUser()::getSshKeys,
+ return new ScimCollectionConverter<>(user.getIndigoUser()::getSshKeys,
sshKeyConverter::fromScim);
}
private ScimCollectionConverter oidcIdConverter(ScimUser user) {
- return new ScimCollectionConverter(user.getIndigoUser()::getOidcIds,
+ return new ScimCollectionConverter<>(user.getIndigoUser()::getOidcIds,
oidcIdConverter::fromScim);
}
private ScimCollectionConverter samlIdConverter(ScimUser user) {
- return new ScimCollectionConverter(user.getIndigoUser()::getSamlIds,
+ return new ScimCollectionConverter<>(user.getIndigoUser()::getSamlIds,
samlIdConverter::fromScim);
}
private ScimCollectionConverter x509CertificateConverter(
ScimUser user) {
- return new ScimCollectionConverter(
- user::getX509Certificates, x509CertificateConverter::fromScim);
+ return new ScimCollectionConverter<>(
+ user.getIndigoUser()::getCertificates, x509CertificateConverter::fromScim);
}
- private static AccountUpdater buildUpdater(AccountUpdaterBuilder factory, Supplier valueSupplier) {
+ private static AccountUpdater buildUpdater(AccountUpdaterBuilder factory,
+ Supplier valueSupplier) {
return factory.build(valueSupplier.get());
}
@@ -188,7 +190,7 @@ private void prepareReplacers(List updaters, ScimUser user, IamA
@Override
public List getUpdatersForPatchOperation(IamAccount account,
- ScimPatchOperation op) {
+ ScimPatchOperation op) throws ScimPatchOperationNotSupported {
final List updaters = Lists.newArrayList();
@@ -208,6 +210,11 @@ public List getUpdatersForPatchOperation(IamAccount account,
prepareReplacers(updaters, user, account);
}
+
+ if (updaters.isEmpty()) {
+ throw new ScimPatchOperationNotSupported(op.getOp() + " operation not supported");
+ }
+
return updaters;
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/util/CollectionHelpers.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/util/CollectionHelpers.java
index 8be1a190c..82b924850 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/util/CollectionHelpers.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/util/CollectionHelpers.java
@@ -6,8 +6,12 @@
public class CollectionHelpers {
+ private CollectionHelpers() {
+ // This class should not be instantiated
+ }
+
public static boolean notNullOrEmpty(Collection c) {
return nonNull(c) && !c.isEmpty();
}
-
+
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/util/IdNotBoundChecker.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/util/IdNotBoundChecker.java
index c7ab7d850..66db2d3f4 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/util/IdNotBoundChecker.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/util/IdNotBoundChecker.java
@@ -2,6 +2,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
+import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
@@ -27,7 +28,9 @@ public IdNotBoundChecker(AccountFinder finder, IamAccount account,
public boolean test(T id) {
checkNotNull(id);
- finder.find(id).ifPresent(otherAccount -> {
+ Optional a = finder.find(id);
+
+ a.ifPresent(otherAccount -> {
if (!otherAccount.equals(account)) {
action.accept(id, account);
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/util/ScimCollectionConverter.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/util/ScimCollectionConverter.java
index 5d8d437c9..24fdf94ef 100644
--- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/util/ScimCollectionConverter.java
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/updater/util/ScimCollectionConverter.java
@@ -28,7 +28,7 @@ public Collection get() {
return supplier.get()
.stream()
.filter(Objects::nonNull)
- .map(i -> converter.fromScim(i))
+ .map(converter::fromScim)
.collect(Collectors.toList());
}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/AccessTokensController.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/AccessTokensController.java
new file mode 100644
index 000000000..98c25d8d7
--- /dev/null
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/AccessTokensController.java
@@ -0,0 +1,93 @@
+package it.infn.mw.iam.api.tokens;
+
+import static it.infn.mw.iam.api.tokens.Constants.ACCESS_TOKENS_ENDPOINT;
+
+import it.infn.mw.iam.api.account.authority.ErrorDTO;
+import it.infn.mw.iam.api.tokens.exception.TokenNotFoundException;
+import it.infn.mw.iam.api.tokens.model.AccessToken;
+import it.infn.mw.iam.api.tokens.model.TokensListResponse;
+import it.infn.mw.iam.api.tokens.service.TokenService;
+import it.infn.mw.iam.api.tokens.service.paging.TokensPageRequest;
+import it.infn.mw.iam.core.user.exception.IamAccountException;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.converter.json.MappingJacksonValue;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Optional;
+
+@RestController
+@Transactional
+@PreAuthorize("hasRole('ADMIN')")
+@RequestMapping(ACCESS_TOKENS_ENDPOINT)
+public class AccessTokensController extends TokensControllerSupport {
+
+ @Autowired
+ private TokenService tokenService;
+
+ @RequestMapping(method = RequestMethod.GET, produces = CONTENT_TYPE)
+ public MappingJacksonValue listAccessTokens(@RequestParam(required = false) Integer count,
+ @RequestParam(required = false) Integer startIndex,
+ @RequestParam(required = false) String userId,
+ @RequestParam(required = false) String clientId,
+ @RequestParam(required = false) final String attributes) {
+
+ TokensPageRequest pr = buildTokensPageRequest(count, startIndex);
+ TokensListResponse results = getFilteredList(pr, userId, clientId);
+ return filterAttributes(results, attributes);
+ }
+
+ private TokensListResponse getFilteredList(TokensPageRequest pageRequest,
+ String userId, String clientId) {
+
+ Optional user = Optional.ofNullable(userId);
+ Optional client = Optional.ofNullable(clientId);
+
+ if (user.isPresent() && client.isPresent()) {
+ return tokenService.getTokensForClientAndUser(user.get(), client.get(), pageRequest);
+ }
+ if (user.isPresent()) {
+ return tokenService.getTokensForUser(user.get(), pageRequest);
+ }
+ if (client.isPresent()) {
+ return tokenService.getTokensForClient(client.get(), pageRequest);
+ }
+ return tokenService.getAllTokens(pageRequest);
+ }
+
+ @RequestMapping(method = RequestMethod.GET, value = "/{id}", produces = CONTENT_TYPE)
+ public AccessToken getAccessToken(@PathVariable("id") Long id) {
+
+ return tokenService.getTokenById(id);
+ }
+
+ @RequestMapping(method = RequestMethod.DELETE, value = "/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void revokeAccessToken(@PathVariable("id") Long id) {
+
+ tokenService.revokeTokenById(id);
+ }
+
+ @ResponseStatus(value = HttpStatus.NOT_FOUND)
+ @ExceptionHandler(TokenNotFoundException.class)
+ public ErrorDTO tokenNotFoundError(Exception ex) {
+
+ return ErrorDTO.fromString(ex.getMessage());
+ }
+
+ @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
+ @ExceptionHandler(IamAccountException.class)
+ public ErrorDTO accountNotFoundError(Exception ex) {
+
+ return ErrorDTO.fromString(ex.getMessage());
+ }
+}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/Constants.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/Constants.java
new file mode 100644
index 000000000..c8feeb89f
--- /dev/null
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/Constants.java
@@ -0,0 +1,11 @@
+package it.infn.mw.iam.api.tokens;
+
+public class Constants {
+
+ public static final String ACCESS_TOKENS_ENDPOINT = "/iam/api/access-tokens";
+ public static final String REFRESH_TOKENS_ENDPOINT = "/iam/api/refresh-tokens";
+
+ private Constants() {
+ // utility class, it should not be instantiated
+ }
+}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/DefaultTokensResourceLocationProvider.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/DefaultTokensResourceLocationProvider.java
new file mode 100644
index 000000000..29bd73271
--- /dev/null
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/DefaultTokensResourceLocationProvider.java
@@ -0,0 +1,25 @@
+package it.infn.mw.iam.api.tokens;
+
+import static it.infn.mw.iam.api.tokens.Constants.ACCESS_TOKENS_ENDPOINT;
+import static it.infn.mw.iam.api.tokens.Constants.REFRESH_TOKENS_ENDPOINT;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Service
+public class DefaultTokensResourceLocationProvider implements TokensResourceLocationProvider {
+
+ @Value("${iam.baseUrl}")
+ private String baseUrl;
+
+ @Override
+ public String accessTokenLocation(Long accessTokenId) {
+ return String.format("%s%s/%d", baseUrl, ACCESS_TOKENS_ENDPOINT, accessTokenId);
+ }
+
+ @Override
+ public String refreshTokenLocation(Long refreshTokenId) {
+ return String.format("%s%s/%d", baseUrl, REFRESH_TOKENS_ENDPOINT, refreshTokenId);
+ }
+
+}
diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/RefreshTokensController.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/RefreshTokensController.java
new file mode 100644
index 000000000..369e26954
--- /dev/null
+++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/RefreshTokensController.java
@@ -0,0 +1,94 @@
+package it.infn.mw.iam.api.tokens;
+
+import static it.infn.mw.iam.api.tokens.Constants.REFRESH_TOKENS_ENDPOINT;
+
+import it.infn.mw.iam.api.account.authority.ErrorDTO;
+import it.infn.mw.iam.api.tokens.exception.TokenNotFoundException;
+import it.infn.mw.iam.api.tokens.model.RefreshToken;
+import it.infn.mw.iam.api.tokens.model.TokensListResponse;
+import it.infn.mw.iam.api.tokens.service.TokenService;
+import it.infn.mw.iam.api.tokens.service.paging.TokensPageRequest;
+import it.infn.mw.iam.core.user.exception.IamAccountException;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.converter.json.MappingJacksonValue;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Optional;
+
+@RestController
+@Transactional
+@PreAuthorize("hasRole('ADMIN')")
+@RequestMapping(REFRESH_TOKENS_ENDPOINT)
+public class RefreshTokensController extends TokensControllerSupport {
+
+ @Autowired
+ private TokenService