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 tokenService; + + @RequestMapping(method = RequestMethod.GET, produces = CONTENT_TYPE) + public MappingJacksonValue lisRefreshTokens(@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 RefreshToken getRefreshToken(@PathVariable("id") Long id) { + + return tokenService.getTokenById(id); + } + + @RequestMapping(method = RequestMethod.DELETE, value = "/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void revokeRefreshToken(@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/TokensControllerSupport.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/TokensControllerSupport.java new file mode 100644 index 000000000..0c9ed4745 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/TokensControllerSupport.java @@ -0,0 +1,91 @@ +package it.infn.mw.iam.api.tokens; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.Sets; + +import com.fasterxml.jackson.databind.ser.FilterProvider; +import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; +import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; +import it.infn.mw.iam.api.tokens.model.TokensListResponse; +import it.infn.mw.iam.api.tokens.service.paging.DefaultTokensPageRequest; +import it.infn.mw.iam.api.tokens.service.paging.TokensPageRequest; + +import org.springframework.http.converter.json.MappingJacksonValue; + +import java.util.HashSet; +import java.util.Set; + +public class TokensControllerSupport { + + public static final String CONTENT_TYPE = "application/json"; + public static final int TOKENS_MAX_PAGE_SIZE = 20; + + protected TokensPageRequest buildTokensPageRequest(Integer count, Integer startIndex) { + return buildPageRequest(count, startIndex, TOKENS_MAX_PAGE_SIZE); + } + + private TokensPageRequest 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; + } + } + + // tokens pages index is 1-based + if (startIndex == null) { + validStartIndex = 1; + + } else { + + validStartIndex = startIndex; + if (startIndex < 1) { + validStartIndex = 1; + } + } + + return new DefaultTokensPageRequest.Builder().count(validCount) + .startIndex(validStartIndex - 1) + .build(); + } + + protected 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.add("id"); + return result; + } + + protected MappingJacksonValue filterAttributes(TokensListResponse result, + String attributes) { + + MappingJacksonValue wrapper = new MappingJacksonValue(result); + + if (attributes != null) { + Set includeAttributes = parseAttributes(attributes); + + FilterProvider filterProvider = new SimpleFilterProvider().addFilter("attributeFilter", + SimpleBeanPropertyFilter.filterOutAllExcept(includeAttributes)); + + wrapper.setFilters(filterProvider); + } + + return wrapper; + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/TokensResourceLocationProvider.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/TokensResourceLocationProvider.java new file mode 100644 index 000000000..e5f21686f --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/TokensResourceLocationProvider.java @@ -0,0 +1,8 @@ +package it.infn.mw.iam.api.tokens; + +public interface TokensResourceLocationProvider { + + public String accessTokenLocation(Long accessTokenId); + + public String refreshTokenLocation(Long refreshTokenId); +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/converter/TokensConverter.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/converter/TokensConverter.java new file mode 100644 index 000000000..d1762ab5f --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/converter/TokensConverter.java @@ -0,0 +1,119 @@ +package it.infn.mw.iam.api.tokens.converter; + +import it.infn.mw.iam.api.scim.converter.ScimResourceLocationProvider; +import it.infn.mw.iam.api.tokens.TokensResourceLocationProvider; +import it.infn.mw.iam.api.tokens.model.AccessToken; +import it.infn.mw.iam.api.tokens.model.ClientRef; +import it.infn.mw.iam.api.tokens.model.IdTokenRef; +import it.infn.mw.iam.api.tokens.model.RefreshToken; +import it.infn.mw.iam.api.tokens.model.UserRef; +import it.infn.mw.iam.core.user.exception.IamAccountException; +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.repository.IamAccountRepository; + +import org.mitre.oauth2.model.AuthenticationHolderEntity; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.model.SavedUserAuthentication; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class TokensConverter { + + @Autowired + private ClientDetailsEntityService clientDetailsService; + + @Autowired + private IamAccountRepository accountRepository; + + @Autowired + private TokensResourceLocationProvider tokensResourceLocationProvider; + + @Autowired + private ScimResourceLocationProvider scimResourceLocationProvider; + + public AccessToken toAccessToken(OAuth2AccessTokenEntity at) { + + AuthenticationHolderEntity ah = at.getAuthenticationHolder(); + + ClientRef clientRef = buildClientRef(ah.getClientId()); + UserRef userRef = buildUserRef(ah.getUserAuth()); + IdTokenRef idTokenRef = buildIdTokenRef(at.getIdToken()); + + return AccessToken.builder() + .id(at.getId()) + .client(clientRef) + .expiration(at.getExpiration()) + .idToken(idTokenRef) + .scopes(at.getScope()) + .user(userRef) + .value(at.getValue()) + .build(); + } + + public RefreshToken toRefreshToken(OAuth2RefreshTokenEntity rt) { + + AuthenticationHolderEntity ah = rt.getAuthenticationHolder(); + + ClientRef clientRef = buildClientRef(ah.getClientId()); + UserRef userRef = buildUserRef(ah.getUserAuth()); + + return RefreshToken.builder() + .id(rt.getId()) + .client(clientRef) + .expiration(rt.getExpiration()) + .user(userRef) + .value(rt.getValue()) + .build(); + } + + + private ClientRef buildClientRef(String clientId) { + + if (clientId == null) { + return null; + } + + ClientDetailsEntity cd = clientDetailsService.loadClientByClientId(clientId); + + return ClientRef.builder() + .id(cd.getId()) + .clientId(cd.getClientId()) + .contacts(cd.getContacts()) + .ref(cd.getClientUri()) + .build(); + } + + private UserRef buildUserRef(SavedUserAuthentication userAuth) { + + if (userAuth == null) { + return null; + } + + String username = userAuth.getPrincipal().toString(); + + IamAccount account = accountRepository.findByUsername(username) + .orElseThrow(() -> new IamAccountException("Account for " + username + " not found")); + + return UserRef.builder() + .id(account.getUuid()) + .userName(account.getUsername()) + .ref(scimResourceLocationProvider.userLocation(account.getUuid())) + .build(); + } + + private IdTokenRef buildIdTokenRef(OAuth2AccessTokenEntity idToken) { + + if (idToken == null) { + return null; + } + + return IdTokenRef.builder() + .id(idToken.getId()) + .ref(tokensResourceLocationProvider.accessTokenLocation(idToken.getId())) + .build(); + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/exception/TokenNotFoundException.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/exception/TokenNotFoundException.java new file mode 100644 index 000000000..19abe1e46 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/exception/TokenNotFoundException.java @@ -0,0 +1,21 @@ +package it.infn.mw.iam.api.tokens.exception; + +public class TokenNotFoundException extends RuntimeException { + + private final Long tokenId; + + /** + * + */ + private static final long serialVersionUID = 1L; + + public TokenNotFoundException(Long tokenId) { + super("Token with id = " + tokenId + " not found"); + this.tokenId = tokenId; + } + + public Long getTokenId() { + return tokenId; + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/model/AccessToken.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/model/AccessToken.java new file mode 100644 index 000000000..ff69225e1 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/model/AccessToken.java @@ -0,0 +1,151 @@ +package it.infn.mw.iam.api.tokens.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Date; +import java.util.Set; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonFilter("attributeFilter") +public class AccessToken { + + private Long id; + private String value; + private Set scopes; + private Date expiration; + private ClientRef client; + private UserRef user; + private IdTokenRef idToken; + + @JsonCreator + public AccessToken(@JsonProperty("id") Long id, @JsonProperty("value") String value, + @JsonProperty("scopes") Set scopes, @JsonProperty("expiration") Date expiration, + @JsonProperty("client") ClientRef client, @JsonProperty("user") UserRef user, + @JsonProperty("idToken") IdTokenRef idToken) { + + this.id = id; + this.value = value; + this.scopes = scopes; + this.expiration = expiration; + this.client = client; + this.user = user; + this.idToken = idToken; + } + + public AccessToken(Builder builder) { + + this.id = builder.id; + this.value = builder.value; + this.scopes = builder.scopes; + this.expiration = builder.expiration; + this.client = builder.client; + this.user = builder.user; + this.idToken = builder.idToken; + } + + @JsonProperty("id") + public Long getId() { + + return id; + } + + @JsonProperty("value") + public String getValue() { + + return value; + } + + @JsonProperty("scopes") + public Set getScopes() { + + return scopes; + } + + @JsonProperty("expiration") + public Date getExpiration() { + + return expiration; + } + + @JsonProperty("client") + public ClientRef getClient() { + + return client; + } + + @JsonProperty("user") + public UserRef getUser() { + + return user; + } + + @JsonProperty("idToken") + public IdTokenRef getIdToken() { + + return idToken; + } + + @Override + public String toString() { + return "AccessToken [id=" + id + ", value=" + value + ", scopes=" + scopes + ", expiration=" + + expiration + ", client=" + client + ", user=" + user + ", idToken=" + idToken + "]"; + } + + public static Builder builder() { + + return new Builder(); + } + + public static class Builder { + + private Long id; + private String value; + private Set scopes; + private Date expiration; + private ClientRef client; + private UserRef user; + private IdTokenRef idToken; + + public Builder id(Long id) { + this.id = id; + return this; + } + + public Builder value(String value) { + this.value = value; + return this; + } + + public Builder scopes(Set scopes) { + this.scopes = scopes; + return this; + } + + public Builder expiration(Date expiration) { + this.expiration = expiration; + return this; + } + + public Builder client(ClientRef client) { + this.client = client; + return this; + } + + public Builder user(UserRef user) { + this.user = user; + return this; + } + + public Builder idToken(IdTokenRef idToken) { + this.idToken = idToken; + return this; + } + + public AccessToken build() { + return new AccessToken(this); + } + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/model/ClientRef.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/model/ClientRef.java new file mode 100644 index 000000000..19f1e823c --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/model/ClientRef.java @@ -0,0 +1,102 @@ +package it.infn.mw.iam.api.tokens.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Set; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class ClientRef { + + private Long id; + private String clientId; + private Set contacts; + private String ref = null; + + @JsonCreator + public ClientRef(@JsonProperty("id") Long id, @JsonProperty("clientId") String clientId, + @JsonProperty("contacts") Set contacts, @JsonProperty("$ref") String ref) { + + this.id = id; + this.clientId = clientId; + this.contacts = contacts; + this.ref = ref; + } + + public ClientRef(Builder builder) { + + this.id = builder.id; + this.clientId = builder.clientId; + this.contacts = builder.contacts; + this.ref = builder.ref; + } + + @JsonProperty("id") + public Long getId() { + + return id; + } + + @JsonProperty("clientId") + public String getClientId() { + + return clientId; + } + + @JsonProperty("contacts") + public Set getContacts() { + + return contacts; + } + + @JsonProperty("$ref") + public String getRef() { + + return ref; + } + + @Override + public String toString() { + + return "Client [id=" + id + ", clientId=" + clientId + ", contacts=" + contacts + + ", ref=" + ref + "]"; + } + + public static Builder builder() { + + return new Builder(); + } + + public static class Builder { + + private Long id; + private String clientId; + private Set contacts; + private String ref; + + public Builder id(Long id) { + this.id = id; + return this; + } + + public Builder clientId(String clientId) { + this.clientId = clientId; + return this; + } + + public Builder contacts(Set contacts) { + this.contacts = contacts; + return this; + } + + public Builder ref(String ref) { + this.ref = ref; + return this; + } + + public ClientRef build() { + return new ClientRef(this); + } + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/model/IdTokenRef.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/model/IdTokenRef.java new file mode 100644 index 000000000..ed64692c1 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/model/IdTokenRef.java @@ -0,0 +1,68 @@ +package it.infn.mw.iam.api.tokens.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class IdTokenRef { + + private Long id; + private String ref; + + @JsonCreator + public IdTokenRef(@JsonProperty("id") Long id, @JsonProperty("$ref") String ref) { + + this.id = id; + this.ref = ref; + } + + public IdTokenRef(Builder builder) { + + this.id = builder.id; + this.ref = builder.ref; + } + + @JsonProperty("id") + public Long getId() { + + return id; + } + + @JsonProperty("$ref") + public String getRef() { + + return ref; + } + + @Override + public String toString() { + return "IdToken [id=" + id + ", ref=" + ref + "]"; + } + + public static Builder builder() { + + return new Builder(); + } + + public static class Builder { + + private Long id; + private String ref; + + public Builder id(Long id) { + this.id = id; + return this; + } + + public Builder ref(String ref) { + this.ref = ref; + return this; + } + + public IdTokenRef build() { + return new IdTokenRef(this); + } + } +} + diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/model/RefreshToken.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/model/RefreshToken.java new file mode 100644 index 000000000..06ac35a47 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/model/RefreshToken.java @@ -0,0 +1,109 @@ +package it.infn.mw.iam.api.tokens.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Date; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonFilter("attributeFilter") +public class RefreshToken { + + private Long id; + private String value; + private Date expiration; + private ClientRef client; + private UserRef user; + + @JsonCreator + public RefreshToken(@JsonProperty("id") Long id, @JsonProperty("value") String value, + @JsonProperty("expiration") Date expiration, @JsonProperty("client") ClientRef client, + @JsonProperty("user") UserRef user) { + + this.id = id; + this.value = value; + this.expiration = expiration; + this.client = client; + this.user = user; + } + + public RefreshToken(Builder builder) { + + this.id = builder.id; + this.value = builder.value; + this.expiration = builder.expiration; + this.client = builder.client; + this.user = builder.user; + } + + @JsonProperty("id") + public Long getId() { + return id; + } + + @JsonProperty("value") + public String getValue() { + return value; + } + + @JsonProperty("expiration") + public Date getExpiration() { + return expiration; + } + + @JsonProperty("client") + public ClientRef getClient() { + return client; + } + + @JsonProperty("user") + public UserRef getUser() { + return user; + } + + public static Builder builder() { + + return new Builder(); + } + + public static class Builder { + + private Long id; + private String value; + private Date expiration; + private ClientRef client; + private UserRef user; + + public Builder id(Long id) { + this.id = id; + return this; + } + + public Builder value(String value) { + this.value = value; + return this; + } + + public Builder expiration(Date expiration) { + this.expiration = expiration; + return this; + } + + public Builder client(ClientRef client) { + this.client = client; + return this; + } + + public Builder user(UserRef user) { + this.user = user; + return this; + } + + public RefreshToken build() { + return new RefreshToken(this); + } + } +} + diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/model/TokensListResponse.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/model/TokensListResponse.java new file mode 100644 index 000000000..46181b81f --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/model/TokensListResponse.java @@ -0,0 +1,46 @@ +package it.infn.mw.iam.api.tokens.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; + +public class TokensListResponse { + + private long totalResults; + private long itemsPerPage; + private long startIndex; + private List resources = new ArrayList<>(); + + public TokensListResponse() {} + + public TokensListResponse(List resources, long totalResults, long itemsPerPage, + long startIndex) { + + this.resources = resources; + this.totalResults = totalResults; + this.itemsPerPage = itemsPerPage; + this.startIndex = startIndex; + } + + public long getTotalResults() { + + return totalResults; + } + + public long getItemsPerPage() { + + return itemsPerPage; + } + + public long getStartIndex() { + + return startIndex; + } + + @JsonProperty("Resources") + public List getResources() { + + return resources; + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/model/UserRef.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/model/UserRef.java new file mode 100644 index 000000000..c2178a253 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/model/UserRef.java @@ -0,0 +1,80 @@ +package it.infn.mw.iam.api.tokens.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class UserRef { + + private String id; + private String userName; + private String ref; + + @JsonCreator + public UserRef(@JsonProperty("id") String id, @JsonProperty("userName") String userName, + @JsonProperty("$ref") String ref) { + + this.id = id; + this.userName = userName; + this.ref = ref; + } + + public UserRef(Builder builder) { + + this.id = builder.id; + this.userName = builder.userName; + this.ref = builder.ref; + } + + @JsonProperty("id") + public String getId() { + return id; + } + + @JsonProperty("userName") + public String getUserName() { + return userName; + } + + @JsonProperty("$ref") + public String getRef() { + return ref; + } + + @Override + public String toString() { + return "User [id=" + id + ", userName=" + userName + ", ref=" + ref + "]"; + } + + public static Builder builder() { + + return new Builder(); + } + + public static class Builder { + + private String id; + private String userName; + private String ref; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder userName(String userName) { + this.userName = userName; + return this; + } + + public Builder ref(String ref) { + this.ref = ref; + return this; + } + + public UserRef build() { + return new UserRef(this); + } + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/service/DefaultAccessTokenService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/service/DefaultAccessTokenService.java new file mode 100644 index 000000000..530f2f44a --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/service/DefaultAccessTokenService.java @@ -0,0 +1,123 @@ +package it.infn.mw.iam.api.tokens.service; + +import it.infn.mw.iam.api.tokens.converter.TokensConverter; +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.paging.OffsetPageable; +import it.infn.mw.iam.api.tokens.service.paging.TokensPageRequest; +import it.infn.mw.iam.persistence.repository.IamOAuthAccessTokenRepository; + +import org.mitre.oauth2.model.OAuth2AccessTokenEntity; +import org.mitre.oauth2.service.impl.DefaultOAuth2ProviderTokenService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +@Service +public class DefaultAccessTokenService implements TokenService { + + @Autowired + private TokensConverter tokensConverter; + + @Autowired + private DefaultOAuth2ProviderTokenService tokenService; + + @Autowired + private IamOAuthAccessTokenRepository tokenRepository; + + @Override + public AccessToken getTokenById(Long id) { + + OAuth2AccessTokenEntity at = + getAccessTokenById(id).orElseThrow(() -> new TokenNotFoundException(id)); + return tokensConverter.toAccessToken(at); + } + + @Override + public void revokeTokenById(Long id) { + + OAuth2AccessTokenEntity at = + getAccessTokenById(id).orElseThrow(() -> new TokenNotFoundException(id)); + tokenService.revokeAccessToken(at); + } + + private Optional getAccessTokenById(Long accessTokenId) { + + OAuth2AccessTokenEntity at = tokenService.getAccessTokenById(accessTokenId); + return Optional.ofNullable(at); + } + + private TokensListResponse buildTokensCountResponse() { + + return new TokensListResponse<>(Collections.emptyList(), tokenRepository.count(), 0, 1); + } + + private TokensListResponse buildTokensListResponse( + Page entities, OffsetPageable op) { + + List resources = new ArrayList<>(); + + entities.getContent().forEach(a -> resources.add(tokensConverter.toAccessToken(a))); + + return new TokensListResponse<>(resources, entities.getTotalElements(), resources.size(), + op.getOffset() + 1); + } + + @Override + public TokensListResponse getAllTokens(TokensPageRequest pageRequest) { + + if (pageRequest.getCount() == 0) { + return buildTokensCountResponse(); + } + + OffsetPageable op = new OffsetPageable(pageRequest.getStartIndex(), pageRequest.getCount()); + return buildTokensListResponse(tokenRepository.findAllValidAccessTokens(new Date(), op), op); + } + + @Override + public TokensListResponse getTokensForUser(String userId, + TokensPageRequest pageRequest) { + + if (pageRequest.getCount() == 0) { + return buildTokensCountResponse(); + } + + OffsetPageable op = new OffsetPageable(pageRequest.getStartIndex(), pageRequest.getCount()); + return buildTokensListResponse( + tokenRepository.findValidAccessTokensForUser(userId, new Date(), op), op); + } + + @Override + public TokensListResponse getTokensForClient(String clientId, + TokensPageRequest pageRequest) { + + if (pageRequest.getCount() == 0) { + return buildTokensCountResponse(); + } + + OffsetPageable op = new OffsetPageable(pageRequest.getStartIndex(), pageRequest.getCount()); + return buildTokensListResponse( + tokenRepository.findValidAccessTokensForClient(clientId, new Date(), op), op); + } + + @Override + public TokensListResponse getTokensForClientAndUser(String userId, String clientId, + TokensPageRequest pageRequest) { + + if (pageRequest.getCount() == 0) { + return buildTokensCountResponse(); + } + + OffsetPageable op = new OffsetPageable(pageRequest.getStartIndex(), pageRequest.getCount()); + return buildTokensListResponse( + tokenRepository.findValidAccessTokensForUserAndClient(userId, clientId, new Date(), op), + op); + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/service/DefaultRefreshTokenService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/service/DefaultRefreshTokenService.java new file mode 100644 index 000000000..39d13fccb --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/service/DefaultRefreshTokenService.java @@ -0,0 +1,123 @@ +package it.infn.mw.iam.api.tokens.service; + +import it.infn.mw.iam.api.tokens.converter.TokensConverter; +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.paging.OffsetPageable; +import it.infn.mw.iam.api.tokens.service.paging.TokensPageRequest; +import it.infn.mw.iam.persistence.repository.IamOAuthRefreshTokenRepository; + +import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; +import org.mitre.oauth2.service.impl.DefaultOAuth2ProviderTokenService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +@Service +public class DefaultRefreshTokenService implements TokenService { + + @Autowired + private TokensConverter tokensConverter; + + @Autowired + private DefaultOAuth2ProviderTokenService tokenService; + + @Autowired + private IamOAuthRefreshTokenRepository tokenRepository; + + @Override + public RefreshToken getTokenById(Long id) { + + OAuth2RefreshTokenEntity rt = + getRefreshTokenById(id).orElseThrow(() -> new TokenNotFoundException(id)); + return tokensConverter.toRefreshToken(rt); + } + + @Override + public void revokeTokenById(Long id) { + + OAuth2RefreshTokenEntity rt = + getRefreshTokenById(id).orElseThrow(() -> new TokenNotFoundException(id)); + tokenService.revokeRefreshToken(rt); + } + + private Optional getRefreshTokenById(Long refreshTokenId) { + + OAuth2RefreshTokenEntity at = tokenService.getRefreshTokenById(refreshTokenId); + return Optional.ofNullable(at); + } + + private TokensListResponse buildTokensCountResponse() { + + return new TokensListResponse<>(Collections.emptyList(), tokenRepository.count(), 0, 1); + } + + private TokensListResponse buildTokensListResponse( + Page entities, OffsetPageable op) { + + List resources = new ArrayList<>(); + + entities.getContent().forEach(a -> resources.add(tokensConverter.toRefreshToken(a))); + + return new TokensListResponse<>(resources, entities.getTotalElements(), resources.size(), + op.getOffset() + 1); + } + + @Override + public TokensListResponse getAllTokens(TokensPageRequest pageRequest) { + + if (pageRequest.getCount() == 0) { + return buildTokensCountResponse(); + } + + OffsetPageable op = new OffsetPageable(pageRequest.getStartIndex(), pageRequest.getCount()); + return buildTokensListResponse(tokenRepository.findAllValidRefreshTokens(new Date(), op), op); + } + + @Override + public TokensListResponse getTokensForUser(String userId, + TokensPageRequest pageRequest) { + + if (pageRequest.getCount() == 0) { + return buildTokensCountResponse(); + } + + OffsetPageable op = new OffsetPageable(pageRequest.getStartIndex(), pageRequest.getCount()); + return buildTokensListResponse( + tokenRepository.findValidRefreshTokensForUser(userId, new Date(), op), op); + } + + @Override + public TokensListResponse getTokensForClient(String clientId, + TokensPageRequest pageRequest) { + + if (pageRequest.getCount() == 0) { + return buildTokensCountResponse(); + } + + OffsetPageable op = new OffsetPageable(pageRequest.getStartIndex(), pageRequest.getCount()); + return buildTokensListResponse( + tokenRepository.findValidRefreshTokensForClient(clientId, new Date(), op), op); + } + + @Override + public TokensListResponse getTokensForClientAndUser(String userId, String clientId, + TokensPageRequest pageRequest) { + + if (pageRequest.getCount() == 0) { + return buildTokensCountResponse(); + } + + OffsetPageable op = new OffsetPageable(pageRequest.getStartIndex(), pageRequest.getCount()); + return buildTokensListResponse( + tokenRepository.findValidRefreshTokensForUserAndClient(userId, clientId, new Date(), op), + op); + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/service/TokenService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/service/TokenService.java new file mode 100644 index 000000000..12d24d79a --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/service/TokenService.java @@ -0,0 +1,22 @@ +package it.infn.mw.iam.api.tokens.service; + +import it.infn.mw.iam.api.tokens.model.TokensListResponse; +import it.infn.mw.iam.api.tokens.service.paging.TokensPageRequest; + +public interface TokenService { + + TokensListResponse getAllTokens(final TokensPageRequest pageRequest); + + TokensListResponse getTokensForUser(final String userId, final TokensPageRequest pageRequest); + + TokensListResponse getTokensForClient(final String clientId, + final TokensPageRequest pageRequest); + + TokensListResponse getTokensForClientAndUser(final String userId, final String clientId, + final TokensPageRequest pageRequest); + + T getTokenById(Long id); + + void revokeTokenById(Long id); + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/service/paging/DefaultTokensPageRequest.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/service/paging/DefaultTokensPageRequest.java new file mode 100644 index 000000000..b42ba3b75 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/service/paging/DefaultTokensPageRequest.java @@ -0,0 +1,47 @@ +package it.infn.mw.iam.api.tokens.service.paging; + +public class DefaultTokensPageRequest implements TokensPageRequest { + + private final int count; + private final int startIndex; + + private DefaultTokensPageRequest(Builder b) { + this.count = b.count; + this.startIndex = b.startIndex; + } + + @Override + public int getCount() { + + return count; + } + + @Override + public int getStartIndex() { + + return startIndex; + } + + public static class Builder { + + private int count; + private int startIndex; + + public Builder count(int count) { + + this.count = count; + return this; + } + + public Builder startIndex(int startIndex) { + + this.startIndex = startIndex; + return this; + } + + public DefaultTokensPageRequest build() { + + return new DefaultTokensPageRequest(this); + } + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/service/paging/OffsetPageable.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/service/paging/OffsetPageable.java new file mode 100644 index 000000000..42791176e --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/service/paging/OffsetPageable.java @@ -0,0 +1,105 @@ +package it.infn.mw.iam.api.tokens.service.paging; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import com.google.common.base.Preconditions; + +public class OffsetPageable implements Pageable { + + private final int offset; + private final int count; + + public OffsetPageable(int offset, int count) { + Preconditions.checkArgument(offset >= 0, "offset must be greater or equal to 0"); + + Preconditions.checkArgument(count >= 1, "count must be a positive integer"); + + this.offset = offset; + this.count = count; + } + + @Override + public int getPageNumber() { + + return offset / count; + } + + @Override + public int getPageSize() { + + return count; + } + + @Override + public int getOffset() { + + return offset; + } + + @Override + public Sort getSort() { + + return null; + } + + @Override + public Pageable next() { + + return new OffsetPageable(offset + count, count); + } + + @Override + public Pageable previousOrFirst() { + + int newOffset = offset - count; + if (newOffset < 0) { + newOffset = 0; + } + + return new OffsetPageable(newOffset, count); + + } + + @Override + public Pageable first() { + + return new OffsetPageable(0, count); + } + + @Override + public boolean hasPrevious() { + + return offset > 0; + } + + @Override + public int hashCode() { + final int PRIME = 31; + int result = 1; + result = PRIME * result + count; + result = PRIME * result + offset; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + OffsetPageable other = (OffsetPageable) obj; + if (count != other.count) + return false; + return offset != other.offset; + } + + @Override + public String toString() { + + return "OffsetPageable [offset=" + offset + ", count=" + count + "]"; + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/service/paging/TokensPageRequest.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/service/paging/TokensPageRequest.java new file mode 100644 index 000000000..bceab19cc --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/tokens/service/paging/TokensPageRequest.java @@ -0,0 +1,9 @@ +package it.infn.mw.iam.api.tokens.service.paging; + +public interface TokensPageRequest { + + public int getCount(); + + public int getStartIndex(); + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/AuditDataSerializer.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/AuditDataSerializer.java index 2225b0b2e..6f7d28fdf 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/audit/AuditDataSerializer.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/AuditDataSerializer.java @@ -1,7 +1,7 @@ package it.infn.mw.iam.audit; import it.infn.mw.iam.audit.events.IamAuditApplicationEvent; - +@FunctionalInterface public interface AuditDataSerializer { public String serialize(IamAuditApplicationEvent event); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/AuditEventLogger.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/AuditEventLogger.java index 87edf6c60..c6ff5b774 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/audit/AuditEventLogger.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/AuditEventLogger.java @@ -2,6 +2,7 @@ import it.infn.mw.iam.audit.events.IamAuditApplicationEvent; +@FunctionalInterface public interface AuditEventLogger { public void logAuditEvent(IamAuditApplicationEvent event); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/IamAuditEventLogger.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/IamAuditEventLogger.java index 07093ac82..297f5699d 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/audit/IamAuditEventLogger.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/IamAuditEventLogger.java @@ -28,7 +28,10 @@ public IamAuditEventLogger(AuditDataSerializer serializer) { @Override public void logAuditEvent(IamAuditApplicationEvent event) { lastEvent = event; - LOG.info(AUDIT_MARKER, serializer.serialize(event)); + if (LOG.isInfoEnabled()){ + final String serializedEvent = serializer.serialize(event); + LOG.info(AUDIT_MARKER, serializedEvent); + } } public IamAuditApplicationEvent getLastEvent() { diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/IamAuditApplicationEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/IamAuditApplicationEvent.java index 58a654803..57d7ce414 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/IamAuditApplicationEvent.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/IamAuditApplicationEvent.java @@ -17,7 +17,7 @@ @JsonTypeInfo(use=Id.NAME, property="@type") public abstract class IamAuditApplicationEvent extends ApplicationEvent { - public static enum IamEventCategory { + public enum IamEventCategory { NONE, ACCOUNT, GROUP, diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/AccountLinkedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/AccountLinkedEvent.java index 436b6c6f8..f2dacae87 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/AccountLinkedEvent.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/AccountLinkedEvent.java @@ -9,7 +9,7 @@ public class AccountLinkedEvent extends AccountEvent { private static final long serialVersionUID = -1605221918249294636L; - @JsonIgnoreProperties({"email", "givenName", "familyName"}) + @JsonIgnoreProperties({"email", "GIVEN_NAME", "familyName"}) private final ExternalAuthenticationRegistrationInfo externalAccountInfo; public AccountLinkedEvent(Object source, IamAccount account, diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/AccountUnlinkedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/AccountUnlinkedEvent.java index b62cc1492..4db9f1cf1 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/AccountUnlinkedEvent.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/AccountUnlinkedEvent.java @@ -9,7 +9,7 @@ public class AccountUnlinkedEvent extends AccountEvent { private static final long serialVersionUID = -1605221918249294636L; - @JsonIgnoreProperties({"email", "givenName", "familyName", "issuer", "subject"}) + @JsonIgnoreProperties({"email", "GIVEN_NAME", "familyName", "issuer", "subject"}) private final ExternalAuthenticationType externalAuthenticationType; private final String issuer; diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/ActiveReplacedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/ActiveReplacedEvent.java new file mode 100644 index 000000000..cf95b4ed5 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/ActiveReplacedEvent.java @@ -0,0 +1,26 @@ +package it.infn.mw.iam.audit.events.account; + +import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REPLACE_ACTIVE; + +import it.infn.mw.iam.api.scim.updater.UpdaterType; +import it.infn.mw.iam.persistence.model.IamAccount; + +public class ActiveReplacedEvent extends AccountUpdatedEvent { + + private static final long serialVersionUID = 5681737929767602266L; + + private final Boolean active; + + public ActiveReplacedEvent(Object source, IamAccount account, Boolean active) { + super(source, account, ACCOUNT_REPLACE_ACTIVE, buildMessage(ACCOUNT_REPLACE_ACTIVE, active)); + this.active = active; + } + + public Boolean getActive() { + return active; + } + + protected static String buildMessage(UpdaterType t, Boolean active) { + return String.format("%s: %s", t.getDescription(), active); + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/EmailReplacedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/EmailReplacedEvent.java new file mode 100644 index 000000000..7f634f5dd --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/EmailReplacedEvent.java @@ -0,0 +1,27 @@ +package it.infn.mw.iam.audit.events.account; + +import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REPLACE_EMAIL; + +import it.infn.mw.iam.api.scim.updater.UpdaterType; +import it.infn.mw.iam.persistence.model.IamAccount; + +public class EmailReplacedEvent extends AccountUpdatedEvent { + + private static final long serialVersionUID = -2527611317336416111L; + + private final String email; + + public EmailReplacedEvent(Object source, IamAccount account, String email) { + super(source, account, ACCOUNT_REPLACE_EMAIL, buildMessage(ACCOUNT_REPLACE_EMAIL, email)); + this.email = email; + } + + public String getEmail() { + return email; + } + + protected static String buildMessage(UpdaterType t, String email) { + return String.format("%s: %s", t.getDescription(), email); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/FamilyNameReplacedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/FamilyNameReplacedEvent.java new file mode 100644 index 000000000..5e1e95df9 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/FamilyNameReplacedEvent.java @@ -0,0 +1,27 @@ +package it.infn.mw.iam.audit.events.account; + +import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REPLACE_FAMILY_NAME; + +import it.infn.mw.iam.api.scim.updater.UpdaterType; +import it.infn.mw.iam.persistence.model.IamAccount; + +public class FamilyNameReplacedEvent extends AccountUpdatedEvent { + + private static final long serialVersionUID = 1984232242208443669L; + + private final String familyName; + + public FamilyNameReplacedEvent(Object source, IamAccount account, String familyName) { + super(source, account, ACCOUNT_REPLACE_FAMILY_NAME, + buildMessage(ACCOUNT_REPLACE_FAMILY_NAME, familyName)); + this.familyName = familyName; + } + + public String getFamilyName() { + return familyName; + } + + protected static String buildMessage(UpdaterType t, String familyName) { + return String.format("%s: %s", t.getDescription(), familyName); + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/GivenNameReplacedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/GivenNameReplacedEvent.java new file mode 100644 index 000000000..09f98a29f --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/GivenNameReplacedEvent.java @@ -0,0 +1,27 @@ +package it.infn.mw.iam.audit.events.account; + +import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REPLACE_GIVEN_NAME; + +import it.infn.mw.iam.api.scim.updater.UpdaterType; +import it.infn.mw.iam.persistence.model.IamAccount; + +public class GivenNameReplacedEvent extends AccountUpdatedEvent { + + private static final long serialVersionUID = -8832461145651484401L; + + private final String givenName; + + public GivenNameReplacedEvent(Object source, IamAccount account, String givenName) { + super(source, account, ACCOUNT_REPLACE_GIVEN_NAME, + buildMessage(ACCOUNT_REPLACE_GIVEN_NAME, givenName)); + this.givenName = givenName; + } + + public String getGivenName() { + return givenName; + } + + protected static String buildMessage(UpdaterType t, String givenName) { + return String.format("%s: %s", t.getDescription(), givenName); + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/PasswordReplacedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/PasswordReplacedEvent.java new file mode 100644 index 000000000..0148cfc3e --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/PasswordReplacedEvent.java @@ -0,0 +1,27 @@ +package it.infn.mw.iam.audit.events.account; + +import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REPLACE_PASSWORD; + +import it.infn.mw.iam.api.scim.updater.UpdaterType; +import it.infn.mw.iam.persistence.model.IamAccount; + +public class PasswordReplacedEvent extends AccountUpdatedEvent { + + private static final long serialVersionUID = -1656076046003614399L; + + private final String password; + + public PasswordReplacedEvent(Object source, IamAccount account, String password) { + super(source, account, ACCOUNT_REPLACE_PASSWORD, + buildMessage(ACCOUNT_REPLACE_PASSWORD, password)); + this.password = password; + } + + public String getPassword() { + return password; + } + + protected static String buildMessage(UpdaterType t, String password) { + return String.format("%s: %s", t.getDescription(), password); + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/PictureRemovedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/PictureRemovedEvent.java new file mode 100644 index 000000000..8145a307a --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/PictureRemovedEvent.java @@ -0,0 +1,26 @@ +package it.infn.mw.iam.audit.events.account; + +import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REMOVE_PICTURE; + +import it.infn.mw.iam.api.scim.updater.UpdaterType; +import it.infn.mw.iam.persistence.model.IamAccount; + +public class PictureRemovedEvent extends AccountUpdatedEvent { + + private static final long serialVersionUID = 4301089543652866437L; + + private final String picture; + + public PictureRemovedEvent(Object source, IamAccount account, String picture) { + super(source, account, ACCOUNT_REMOVE_PICTURE, buildMessage(ACCOUNT_REMOVE_PICTURE, picture)); + this.picture = picture; + } + + public String getPictures() { + return picture; + } + + protected static String buildMessage(UpdaterType t, String picture) { + return String.format("%s: %s", t.getDescription(), picture); + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/PictureReplacedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/PictureReplacedEvent.java new file mode 100644 index 000000000..482f4655a --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/PictureReplacedEvent.java @@ -0,0 +1,26 @@ +package it.infn.mw.iam.audit.events.account; + +import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REPLACE_PICTURE; + +import it.infn.mw.iam.api.scim.updater.UpdaterType; +import it.infn.mw.iam.persistence.model.IamAccount; + +public class PictureReplacedEvent extends AccountUpdatedEvent { + + private static final long serialVersionUID = -2527611317336416111L; + + private final String picture; + + public PictureReplacedEvent(Object source, IamAccount account, String picture) { + super(source, account, ACCOUNT_REPLACE_PICTURE, buildMessage(ACCOUNT_REPLACE_PICTURE, picture)); + this.picture = picture; + } + + public String getPicture() { + return picture; + } + + protected static String buildMessage(UpdaterType t, String picture) { + return String.format("%s: %s", t.getDescription(), picture); + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/UsernameReplacedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/UsernameReplacedEvent.java new file mode 100644 index 000000000..c45be6323 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/UsernameReplacedEvent.java @@ -0,0 +1,27 @@ +package it.infn.mw.iam.audit.events.account; + +import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REPLACE_USERNAME; + +import it.infn.mw.iam.api.scim.updater.UpdaterType; +import it.infn.mw.iam.persistence.model.IamAccount; + +public class UsernameReplacedEvent extends AccountUpdatedEvent { + + private static final long serialVersionUID = -4559709075463515934L; + + private final String username; + + public UsernameReplacedEvent(Object source, IamAccount account, String username) { + super(source, account, ACCOUNT_REPLACE_USERNAME, + buildMessage(ACCOUNT_REPLACE_USERNAME, username)); + this.username = username; + } + + public String getUsername() { + return username; + } + + protected static String buildMessage(UpdaterType t, String username) { + return String.format("%s: %s", t.getDescription(), username); + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/X509CertificateLinkedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/X509CertificateLinkedEvent.java new file mode 100644 index 000000000..a0ff5af16 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/X509CertificateLinkedEvent.java @@ -0,0 +1,24 @@ +package it.infn.mw.iam.audit.events.account; + +import it.infn.mw.iam.authn.x509.IamX509AuthenticationCredential; +import it.infn.mw.iam.persistence.model.IamAccount; + +public class X509CertificateLinkedEvent extends AccountEvent { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final IamX509AuthenticationCredential x509Certificate; + + public X509CertificateLinkedEvent(Object source, IamAccount account, String message, + IamX509AuthenticationCredential cred) { + super(source, account, message); + this.x509Certificate = cred; + } + + public IamX509AuthenticationCredential getX509Certificate() { + return x509Certificate; + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/X509CertificateUnlinkedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/X509CertificateUnlinkedEvent.java new file mode 100644 index 000000000..6b3954dd1 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/X509CertificateUnlinkedEvent.java @@ -0,0 +1,25 @@ +package it.infn.mw.iam.audit.events.account; + +import it.infn.mw.iam.persistence.model.IamAccount; + +public class X509CertificateUnlinkedEvent extends AccountEvent { + + /** + * + */ + private static final long serialVersionUID = 1L; + + + private final String certificateSubject; + + public X509CertificateUnlinkedEvent(Object source, IamAccount account, String message, + String certificateSubject) { + super(source, account, message); + this.certificateSubject = certificateSubject; + } + + public String getCertificateSubject() { + return certificateSubject; + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/X509CertificateUpdatedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/X509CertificateUpdatedEvent.java new file mode 100644 index 000000000..5c0e7a62d --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/X509CertificateUpdatedEvent.java @@ -0,0 +1,13 @@ +package it.infn.mw.iam.audit.events.account; + +import it.infn.mw.iam.authn.x509.IamX509AuthenticationCredential; +import it.infn.mw.iam.persistence.model.IamAccount; + +public class X509CertificateUpdatedEvent extends X509CertificateLinkedEvent { + + public X509CertificateUpdatedEvent(Object source, IamAccount account, String message, + IamX509AuthenticationCredential cred) { + super(source, account, message, cred); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/group/GroupMembershipAddedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/group/GroupMembershipAddedEvent.java new file mode 100644 index 000000000..2eabcabee --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/group/GroupMembershipAddedEvent.java @@ -0,0 +1,19 @@ +package it.infn.mw.iam.audit.events.account.group; + +import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_ADD_GROUP_MEMBERSHIP; + +import java.util.Collection; + +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamGroup; + +public class GroupMembershipAddedEvent extends GroupMembershipUpdatedEvent { + + private static final long serialVersionUID = 1L; + + + public GroupMembershipAddedEvent(Object source, IamAccount account, Collection groups) { + super(source, account, ACCOUNT_ADD_GROUP_MEMBERSHIP, groups); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/group/GroupMembershipRemovedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/group/GroupMembershipRemovedEvent.java new file mode 100644 index 000000000..123e2e172 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/group/GroupMembershipRemovedEvent.java @@ -0,0 +1,19 @@ +package it.infn.mw.iam.audit.events.account.group; + +import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REMOVE_GROUP_MEMBERSHIP; + +import java.util.Collection; + +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamGroup; + +public class GroupMembershipRemovedEvent extends GroupMembershipUpdatedEvent { + + private static final long serialVersionUID = 1L; + + public GroupMembershipRemovedEvent(Object source, IamAccount account, + Collection groups) { + super(source, account, ACCOUNT_REMOVE_GROUP_MEMBERSHIP, groups); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/group/GroupMembershipUpdatedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/group/GroupMembershipUpdatedEvent.java new file mode 100644 index 000000000..128d8fc0e --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/group/GroupMembershipUpdatedEvent.java @@ -0,0 +1,36 @@ +package it.infn.mw.iam.audit.events.account.group; + +import java.util.Collection; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import it.infn.mw.iam.api.scim.updater.UpdaterType; +import it.infn.mw.iam.audit.events.account.AccountUpdatedEvent; +import it.infn.mw.iam.audit.utils.IamGroupCollectionSerializer; +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamGroup; + +public abstract class GroupMembershipUpdatedEvent extends AccountUpdatedEvent { + + private static final long serialVersionUID = 1L; + + @JsonSerialize(using = IamGroupCollectionSerializer.class) + private final Collection groups; + + public GroupMembershipUpdatedEvent(Object source, IamAccount account, UpdaterType type, + Collection groups) { + super(source, account, type, buildMessage(type, account, groups)); + this.groups = groups; + } + + protected Collection getGroups() { + return groups; + } + + protected static String buildMessage(UpdaterType t, IamAccount account, + Collection groups) { + return String.format("%s: username: '%s' values: '%s'", t.getDescription(), + account.getUsername(), groups); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/oidc/OidcAccountAddedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/oidc/OidcAccountAddedEvent.java new file mode 100644 index 000000000..937f0ba20 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/oidc/OidcAccountAddedEvent.java @@ -0,0 +1,18 @@ +package it.infn.mw.iam.audit.events.account.oidc; + +import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_ADD_OIDC_ID; + +import java.util.Collection; + +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamOidcId; + +public class OidcAccountAddedEvent extends OidcAccountUpdatedEvent { + + private static final long serialVersionUID = 1L; + + public OidcAccountAddedEvent(Object source, IamAccount account, Collection oidcIds) { + super(source, account, ACCOUNT_ADD_OIDC_ID, oidcIds); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/oidc/OidcAccountRemovedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/oidc/OidcAccountRemovedEvent.java new file mode 100644 index 000000000..c9772e8f3 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/oidc/OidcAccountRemovedEvent.java @@ -0,0 +1,18 @@ +package it.infn.mw.iam.audit.events.account.oidc; + +import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REMOVE_OIDC_ID; + +import java.util.Collection; + +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamOidcId; + +public class OidcAccountRemovedEvent extends OidcAccountUpdatedEvent { + + private static final long serialVersionUID = 1L; + + public OidcAccountRemovedEvent(Object source, IamAccount account, Collection oidcIds) { + super(source, account, ACCOUNT_REMOVE_OIDC_ID, oidcIds); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/oidc/OidcAccountUpdatedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/oidc/OidcAccountUpdatedEvent.java new file mode 100644 index 000000000..0675aab23 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/oidc/OidcAccountUpdatedEvent.java @@ -0,0 +1,36 @@ +package it.infn.mw.iam.audit.events.account.oidc; + +import java.util.Collection; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import it.infn.mw.iam.api.scim.updater.UpdaterType; +import it.infn.mw.iam.audit.events.account.AccountUpdatedEvent; +import it.infn.mw.iam.audit.utils.IamOidcSerializer; +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamOidcId; + +public abstract class OidcAccountUpdatedEvent extends AccountUpdatedEvent { + + private static final long serialVersionUID = 1L; + + @JsonSerialize(using = IamOidcSerializer.class) + private final Collection oidcIds; + + public OidcAccountUpdatedEvent(Object source, IamAccount account, UpdaterType type, + Collection oidcIds) { + super(source, account, type, buildMessage(type, account, oidcIds)); + this.oidcIds = oidcIds; + } + + protected Collection getOidcIds() { + return oidcIds; + } + + protected static String buildMessage(UpdaterType t, IamAccount account, + Collection oidcIds) { + return String.format("%s: username: '%s' values: '%s'", t.getDescription(), + account.getUsername(), oidcIds); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/saml/SamlAccountAddedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/saml/SamlAccountAddedEvent.java new file mode 100644 index 000000000..c059383a0 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/saml/SamlAccountAddedEvent.java @@ -0,0 +1,17 @@ +package it.infn.mw.iam.audit.events.account.saml; + +import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_ADD_SAML_ID; + +import java.util.Collection; + +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamSamlId; + +public class SamlAccountAddedEvent extends SamlAccountUpdatedEvent { + + private static final long serialVersionUID = 1L; + + public SamlAccountAddedEvent(Object source, IamAccount account, Collection samlIds) { + super(source, account, ACCOUNT_ADD_SAML_ID, samlIds); + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/saml/SamlAccountRemovedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/saml/SamlAccountRemovedEvent.java new file mode 100644 index 000000000..c1a4ae57b --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/saml/SamlAccountRemovedEvent.java @@ -0,0 +1,18 @@ +package it.infn.mw.iam.audit.events.account.saml; + +import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REMOVE_SAML_ID; + +import java.util.Collection; + +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamSamlId; + +public class SamlAccountRemovedEvent extends SamlAccountUpdatedEvent { + + private static final long serialVersionUID = 1L; + + public SamlAccountRemovedEvent(Object source, IamAccount account, Collection samlIds) { + super(source, account, ACCOUNT_REMOVE_SAML_ID, samlIds); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/saml/SamlAccountUpdatedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/saml/SamlAccountUpdatedEvent.java new file mode 100644 index 000000000..796b93b82 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/saml/SamlAccountUpdatedEvent.java @@ -0,0 +1,36 @@ +package it.infn.mw.iam.audit.events.account.saml; + +import java.util.Collection; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import it.infn.mw.iam.api.scim.updater.UpdaterType; +import it.infn.mw.iam.audit.events.account.AccountUpdatedEvent; +import it.infn.mw.iam.audit.utils.IamSamlSerializer; +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamSamlId; + +public abstract class SamlAccountUpdatedEvent extends AccountUpdatedEvent { + + private static final long serialVersionUID = 1L; + + @JsonSerialize(using = IamSamlSerializer.class) + private Collection samlIds; + + public SamlAccountUpdatedEvent(Object source, IamAccount account, UpdaterType type, + Collection samlIds) { + super(source, account, type, buildMessage(type, account, samlIds)); + this.samlIds = samlIds; + } + + protected Collection getSamlIds() { + return samlIds; + } + + protected static String buildMessage(UpdaterType t, IamAccount account, + Collection samlIds) { + return String.format("%s: username: '%s' values '%s'", t.getDescription(), + account.getUsername(), samlIds); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/ssh/SshKeyAddedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/ssh/SshKeyAddedEvent.java new file mode 100644 index 000000000..518c05c51 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/ssh/SshKeyAddedEvent.java @@ -0,0 +1,19 @@ +package it.infn.mw.iam.audit.events.account.ssh; + +import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_ADD_SSH_KEY; + +import java.util.Collection; + +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamSshKey; + +public class SshKeyAddedEvent extends SshKeyUpdatedEvent { + + private static final long serialVersionUID = 1L; + + public SshKeyAddedEvent(Object source, IamAccount account, Collection sshKeys) { + super(source, account, ACCOUNT_ADD_SSH_KEY, sshKeys); + } + + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/ssh/SshKeyRemovedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/ssh/SshKeyRemovedEvent.java new file mode 100644 index 000000000..6c12b31c8 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/ssh/SshKeyRemovedEvent.java @@ -0,0 +1,18 @@ +package it.infn.mw.iam.audit.events.account.ssh; + +import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REMOVE_SSH_KEY; + +import java.util.Collection; + +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamSshKey; + +public class SshKeyRemovedEvent extends SshKeyUpdatedEvent { + + private static final long serialVersionUID = 1L; + + public SshKeyRemovedEvent(Object source, IamAccount account, Collection sshKeys) { + super(source, account, ACCOUNT_REMOVE_SSH_KEY, sshKeys); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/ssh/SshKeyUpdatedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/ssh/SshKeyUpdatedEvent.java new file mode 100644 index 000000000..33e400563 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/ssh/SshKeyUpdatedEvent.java @@ -0,0 +1,36 @@ +package it.infn.mw.iam.audit.events.account.ssh; + +import java.util.Collection; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import it.infn.mw.iam.api.scim.updater.UpdaterType; +import it.infn.mw.iam.audit.events.account.AccountUpdatedEvent; +import it.infn.mw.iam.audit.utils.IamSshKeySerializer; +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamSshKey; + +public abstract class SshKeyUpdatedEvent extends AccountUpdatedEvent { + + private static final long serialVersionUID = 1L; + + @JsonSerialize(using = IamSshKeySerializer.class) + private final Collection sshKeys; + + public SshKeyUpdatedEvent(Object source, IamAccount account, UpdaterType type, + Collection sshKeys) { + super(source, account, type, buildMessage(type, account, sshKeys)); + this.sshKeys = sshKeys; + } + + protected Collection getSshKeys() { + return sshKeys; + } + + protected static String buildMessage(UpdaterType t, IamAccount account, + Collection sshKeys) { + return String.format("%s: username: '%s' values: '%s'", t.getDescription(), + account.getUsername(), sshKeys); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/x509/X509CertificateAddedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/x509/X509CertificateAddedEvent.java new file mode 100644 index 000000000..ffe72afe1 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/x509/X509CertificateAddedEvent.java @@ -0,0 +1,19 @@ +package it.infn.mw.iam.audit.events.account.x509; + +import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_ADD_X509_CERTIFICATE; + +import java.util.Collection; + +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamX509Certificate; + +public class X509CertificateAddedEvent extends X509CertificateUpdatedEvent { + + private static final long serialVersionUID = 1L; + + public X509CertificateAddedEvent(Object source, IamAccount account, + Collection x509certificates) { + super(source, account, ACCOUNT_ADD_X509_CERTIFICATE, x509certificates); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/x509/X509CertificateRemovedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/x509/X509CertificateRemovedEvent.java new file mode 100644 index 000000000..3f5a19c27 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/x509/X509CertificateRemovedEvent.java @@ -0,0 +1,19 @@ +package it.infn.mw.iam.audit.events.account.x509; + +import static it.infn.mw.iam.api.scim.updater.UpdaterType.ACCOUNT_REMOVE_X509_CERTIFICATE; + +import java.util.Collection; + +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamX509Certificate; + +public class X509CertificateRemovedEvent extends X509CertificateUpdatedEvent { + + private static final long serialVersionUID = 1L; + + public X509CertificateRemovedEvent(Object source, IamAccount account, + Collection x509certificates) { + super(source, account, ACCOUNT_REMOVE_X509_CERTIFICATE, x509certificates); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/x509/X509CertificateUpdatedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/x509/X509CertificateUpdatedEvent.java new file mode 100644 index 000000000..fe3fe0a12 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/account/x509/X509CertificateUpdatedEvent.java @@ -0,0 +1,36 @@ +package it.infn.mw.iam.audit.events.account.x509; + +import java.util.Collection; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import it.infn.mw.iam.api.scim.updater.UpdaterType; +import it.infn.mw.iam.audit.events.account.AccountUpdatedEvent; +import it.infn.mw.iam.audit.utils.IamX509CertificateSerializer; +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamX509Certificate; + +public abstract class X509CertificateUpdatedEvent extends AccountUpdatedEvent { + + private static final long serialVersionUID = 1L; + + @JsonSerialize(using = IamX509CertificateSerializer.class) + private final Collection x509Certificates; + + public X509CertificateUpdatedEvent(Object source, IamAccount account, UpdaterType type, + Collection x509Certificates) { + super(source, account, type, buildMessage(type, account, x509Certificates)); + this.x509Certificates = x509Certificates; + } + + protected Collection getX509certificates() { + return x509Certificates; + } + + protected static String buildMessage(UpdaterType t, IamAccount account, + Collection x509Certificates) { + return String.format("%s: username: '%s' values: '%s'", t.getDescription(), + account.getUsername(), x509Certificates); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/group/GroupUpdatedEvent.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/group/GroupUpdatedEvent.java deleted file mode 100644 index ae45b19aa..000000000 --- a/iam-login-service/src/main/java/it/infn/mw/iam/audit/events/group/GroupUpdatedEvent.java +++ /dev/null @@ -1,26 +0,0 @@ -package it.infn.mw.iam.audit.events.group; - -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - -import it.infn.mw.iam.api.scim.updater.UpdaterType; -import it.infn.mw.iam.audit.utils.UpdaterTypeSerializer; -import it.infn.mw.iam.persistence.model.IamGroup; - -public class GroupUpdatedEvent extends GroupEvent { - - private static final long serialVersionUID = -3060371331110710895L; - - @JsonSerialize(using=UpdaterTypeSerializer.class) - private final UpdaterType updaterType; - - public GroupUpdatedEvent(Object source, IamGroup group, UpdaterType updateType, String message) { - super(source, group, message); - this.updaterType = updateType; - } - - - public UpdaterType getUpdaterType() { - return updaterType; - } - -} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamGroupCollectionSerializer.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamGroupCollectionSerializer.java new file mode 100644 index 000000000..072519189 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamGroupCollectionSerializer.java @@ -0,0 +1,29 @@ +package it.infn.mw.iam.audit.utils; + +import java.io.IOException; +import java.util.Collection; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import it.infn.mw.iam.persistence.model.IamGroup; + +public class IamGroupCollectionSerializer extends JsonSerializer> { + + @Override + public void serialize(Collection value, JsonGenerator gen, + SerializerProvider serializers) throws IOException, JsonProcessingException { + + gen.writeStartArray(); + for (IamGroup elem : value) { + gen.writeStartObject(); + gen.writeStringField("uuid", elem.getUuid()); + gen.writeStringField("name", elem.getName()); + gen.writeEndObject(); + } + gen.writeEndArray(); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamGroupSerializer.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamGroupSerializer.java index 990d0fe5e..9528a9128 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamGroupSerializer.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamGroupSerializer.java @@ -14,10 +14,11 @@ public class IamGroupSerializer extends JsonSerializer { @Override public void serialize(IamGroup value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException { - gen.writeStartObject(); - gen.writeStringField("uuid", value.getUuid()); - gen.writeStringField("name", value.getName()); - gen.writeEndObject(); + + gen.writeStartObject(); + gen.writeStringField("uuid", value.getUuid()); + gen.writeStringField("name", value.getName()); + gen.writeEndObject(); } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamOidcSerializer.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamOidcSerializer.java new file mode 100644 index 000000000..9d1d262a9 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamOidcSerializer.java @@ -0,0 +1,28 @@ +package it.infn.mw.iam.audit.utils; + +import java.io.IOException; +import java.util.Collection; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import it.infn.mw.iam.persistence.model.IamOidcId; + +public class IamOidcSerializer extends JsonSerializer> { + + @Override + public void serialize(Collection value, JsonGenerator gen, + SerializerProvider serializers) throws IOException, JsonProcessingException { + gen.writeStartArray(); + for (IamOidcId elem : value) { + gen.writeStartObject(); + gen.writeStringField("issuer", elem.getIssuer()); + gen.writeStringField("subject", elem.getSubject()); + gen.writeEndObject(); + } + gen.writeEndArray(); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamSamlSerializer.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamSamlSerializer.java new file mode 100644 index 000000000..3d803ec02 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamSamlSerializer.java @@ -0,0 +1,29 @@ +package it.infn.mw.iam.audit.utils; + +import java.io.IOException; +import java.util.Collection; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import it.infn.mw.iam.persistence.model.IamSamlId; + +public class IamSamlSerializer extends JsonSerializer> { + + @Override + public void serialize(Collection value, JsonGenerator gen, + SerializerProvider serializers) throws IOException, JsonProcessingException { + + gen.writeStartArray(); + for (IamSamlId elem : value) { + gen.writeStartObject(); + gen.writeStringField("idpid", elem.getIdpId()); + gen.writeStringField("userid", elem.getUserId()); + gen.writeEndObject(); + } + gen.writeEndArray(); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamSshKeySerializer.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamSshKeySerializer.java new file mode 100644 index 000000000..381e954b8 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamSshKeySerializer.java @@ -0,0 +1,29 @@ +package it.infn.mw.iam.audit.utils; + +import java.io.IOException; +import java.util.Collection; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import it.infn.mw.iam.persistence.model.IamSshKey; + +public class IamSshKeySerializer extends JsonSerializer> { + + @Override + public void serialize(Collection value, JsonGenerator gen, + SerializerProvider serializers) throws IOException, JsonProcessingException { + + gen.writeStartArray(); + for (IamSshKey elem : value) { + gen.writeStartObject(); + gen.writeStringField("label", elem.getLabel()); + gen.writeStringField("fingerprint", elem.getFingerprint()); + gen.writeEndObject(); + } + gen.writeEndArray(); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamX509CertificateSerializer.java b/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamX509CertificateSerializer.java new file mode 100644 index 000000000..f582a3abb --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/audit/utils/IamX509CertificateSerializer.java @@ -0,0 +1,31 @@ +package it.infn.mw.iam.audit.utils; + +import java.io.IOException; +import java.util.Collection; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import it.infn.mw.iam.persistence.model.IamX509Certificate; + +public class IamX509CertificateSerializer extends JsonSerializer> { + + @Override + public void serialize(Collection value, JsonGenerator gen, + SerializerProvider serializers) throws IOException, JsonProcessingException { + + gen.writeStartArray(); + for (IamX509Certificate elem : value) { + gen.writeStartObject(); + gen.writeStringField("label", elem.getLabel()); + gen.writeStringField("subjectDn", elem.getSubjectDn()); + gen.writeStringField("issuerDn", elem.getIssuerDn()); + gen.writeStringField("certificate", elem.getCertificate()); + gen.writeEndObject(); + } + gen.writeEndArray(); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/AbstractExternalAuthenticationToken.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/AbstractExternalAuthenticationToken.java index da1a30dd7..ae6873264 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/AbstractExternalAuthenticationToken.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/AbstractExternalAuthenticationToken.java @@ -1,5 +1,6 @@ package it.infn.mw.iam.authn; +import java.io.Serializable; import java.util.Collection; import java.util.Date; import java.util.Map; @@ -9,10 +10,9 @@ import it.infn.mw.iam.persistence.model.IamAccount; -public abstract class AbstractExternalAuthenticationToken +public abstract class AbstractExternalAuthenticationToken extends ExpiringUsernameAuthenticationToken { - /** * */ @@ -32,7 +32,6 @@ public AbstractExternalAuthenticationToken(T authn, Date tokenExpiration, Object this.wrappedAuthentication = authn; } - public T getExternalAuthentication() { return wrappedAuthentication; } @@ -41,6 +40,31 @@ public T getExternalAuthentication() { public abstract void linkToIamAccount(ExternalAccountLinker visitor, IamAccount account); - public abstract ExternalAuthenticationRegistrationInfo toExernalAuthenticationInfo(); + public abstract ExternalAuthenticationRegistrationInfo toExernalAuthenticationRegistrationInfo(); + @Override + public int hashCode() { + int prime = 31; + int result = super.hashCode(); + result = + prime * result + ((wrappedAuthentication == null) ? 0 : wrappedAuthentication.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + AbstractExternalAuthenticationToken other = (AbstractExternalAuthenticationToken) obj; + if (wrappedAuthentication == null) { + if (other.wrappedAuthentication != null) + return false; + } else if (!wrappedAuthentication.equals(other.wrappedAuthentication)) + return false; + return true; + } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/AuthenticationExceptionMessageHelper.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/AuthenticationExceptionMessageHelper.java index 3f60c290f..6a181c4d8 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/AuthenticationExceptionMessageHelper.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/AuthenticationExceptionMessageHelper.java @@ -2,6 +2,7 @@ import org.springframework.security.core.AuthenticationException; +@FunctionalInterface public interface AuthenticationExceptionMessageHelper { String buildErrorMessage(AuthenticationException e); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/DefaultExternalAccountLinker.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/DefaultExternalAccountLinker.java index 1d818e103..ea83057de 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/DefaultExternalAccountLinker.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/DefaultExternalAccountLinker.java @@ -37,19 +37,19 @@ public void linkToIamAccount(IamAccount targetAccount, OidcExternalAuthenticatio if (found.equals(targetAccount)) { - String errorMsg = - String.format("OpenID connect account '[%s] %s' is already linked to user '%s'", - oidcIssuer, oidcSubject, found.getUsername()); + String errorMsg = + String.format("OpenID connect account '[%s] %s' is already linked to user '%s'", + oidcIssuer, oidcSubject, found.getUsername()); - throw new AccountAlreadyLinkedError(errorMsg); + throw new AccountAlreadyLinkedError(errorMsg); } else { - String errorMsg = - String.format("OpenID connect account '[%s] %s' is already linked to another user", - oidcIssuer, oidcSubject); + String errorMsg = + String.format("OpenID connect account '[%s] %s' is already linked to another user", + oidcIssuer, oidcSubject); - throw new AccountAlreadyLinkedError(errorMsg); + throw new AccountAlreadyLinkedError(errorMsg); } }); @@ -65,37 +65,35 @@ public void linkToIamAccount(IamAccount targetAccount, OidcExternalAuthenticatio public void linkToIamAccount(IamAccount targetAccount, SamlExternalAuthenticationToken token) { final SAMLCredential credential = - (SAMLCredential) token.getExternalAuthentication().getCredentials(); + (SAMLCredential) token.getExternalAuthentication().getCredentials(); - final String samlSubject = samlUserIdResolver.getUserIdentifier(credential) + final IamSamlId iamSamlId = samlUserIdResolver.resolveSamlUserIdentifier(credential) + .getResolvedId() .orElseThrow(() -> new UsernameNotFoundException( - "Could not extract a user identifier from the SAML assertion")); + "Could not extract a user identifier from the SAML assertion")); - final String samlIssuer = credential.getRemoteEntityID(); - - repo.findBySamlId(samlIssuer, samlSubject).ifPresent(found -> { + repo.findBySamlId(iamSamlId).ifPresent(found -> { if (found.equals(targetAccount)) { - String errorMsg = String.format("SAML account '[%s] %s' is already linked to user '%s'", - samlIssuer, samlSubject, found.getUsername()); + String errorMsg = String.format( + "SAML account '[%s] (%s = %s)' is already linked to user '%s'", iamSamlId.getIdpId(), + iamSamlId.getAttributeId(), iamSamlId.getUserId(), found.getUsername()); - throw new AccountAlreadyLinkedError(errorMsg); + throw new AccountAlreadyLinkedError(errorMsg); } else { - String errorMsg = String.format("SAML account '[%s] %s' is already linked to another user", - samlIssuer, samlSubject); + String errorMsg = + String.format("SAML account '[%s] (%s = %s)' is already linked to another user", + iamSamlId.getIdpId(), iamSamlId.getAttributeId(), iamSamlId.getUserId()); - throw new AccountAlreadyLinkedError(errorMsg); + throw new AccountAlreadyLinkedError(errorMsg); } }); - IamSamlId samlId = new IamSamlId(); - samlId.setIdpId(samlIssuer); - samlId.setUserId(samlSubject); - samlId.setAccount(targetAccount); - targetAccount.getSamlIds().add(samlId); + iamSamlId.setAccount(targetAccount); + targetAccount.getSamlIds().add(iamSamlId); repo.save(targetAccount); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/DefaultExternalAuthenticationInfoBuilder.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/DefaultExternalAuthenticationInfoBuilder.java index aa540aad2..4e4d7e048 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/DefaultExternalAuthenticationInfoBuilder.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/DefaultExternalAuthenticationInfoBuilder.java @@ -10,7 +10,7 @@ import it.infn.mw.iam.authn.oidc.OidcExternalAuthenticationToken; import it.infn.mw.iam.authn.saml.SamlExternalAuthenticationToken; -import it.infn.mw.iam.authn.saml.util.SamlIdResolvers; +import it.infn.mw.iam.authn.saml.util.Saml2Attribute; @Component public class DefaultExternalAuthenticationInfoBuilder implements ExternalAuthenticationInfoBuilder { @@ -19,7 +19,9 @@ public class DefaultExternalAuthenticationInfoBuilder implements ExternalAuthent public static final String OIDC_TYPE = "oidc"; public static final String SAML_TYPE = "saml"; - public DefaultExternalAuthenticationInfoBuilder() {} + public DefaultExternalAuthenticationInfoBuilder() { + // Empty constructor required by Spring + } public Map buildInfoMap(OidcExternalAuthenticationToken token) { checkNotNull(token, "token cannot be null"); @@ -40,16 +42,14 @@ public Map buildInfoMap(SamlExternalAuthenticationToken token) { result.put(TYPE_ATTR, SAML_TYPE); SAMLCredential cred = (SAMLCredential) token.getExternalAuthentication().getCredentials(); + result.put("idpEntityId", cred.getRemoteEntityID()); - // EPUID - if (cred.getAttributeAsString(SamlIdResolvers.EPUID_NAME) != null) { - result.put("epuid", cred.getAttributeAsString(SamlIdResolvers.EPUID_NAME)); - } - - // EPPN - if (cred.getAttributeAsString(SamlIdResolvers.EPPN_NAME) != null) { - result.put("eppn", cred.getAttributeAsString(SamlIdResolvers.EPPN_NAME)); + for (Saml2Attribute attr: Saml2Attribute.values()){ + String attrVal = cred.getAttributeAsString(attr.getAttributeName()); + if ( attrVal != null) { + result.put(attr.name(), attrVal); + } } return result; diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/DefaultExternalAuthenticationInfoProcessor.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/DefaultExternalAuthenticationInfoProcessor.java index def87d6a6..de43163f5 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/DefaultExternalAuthenticationInfoProcessor.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/DefaultExternalAuthenticationInfoProcessor.java @@ -11,7 +11,9 @@ public class DefaultExternalAuthenticationInfoProcessor implements ExternalAuthenticationInfoProcessor { - public DefaultExternalAuthenticationInfoProcessor() {} + public DefaultExternalAuthenticationInfoProcessor() { + // Empty constructor required by Spring? + } @Override public Map process(OAuth2Authentication authentication) { diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/DefaultInactiveAccountAuthenticationHandler.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/DefaultInactiveAccountAuthenticationHandler.java index 1bc8b6b6c..c2ec2a994 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/DefaultInactiveAccountAuthenticationHandler.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/DefaultInactiveAccountAuthenticationHandler.java @@ -24,18 +24,18 @@ public class DefaultInactiveAccountAuthenticationHandler EnumSet ongoingStatus = EnumSet.of(NEW, CONFIRMED); - public final String WAITING_CONFIRMATION_MSG; - public final String WAITING_APPROVAL_MSG; + public final String waitingConfirmationMsg; + public final String waitingApprovalMsg; @Autowired public DefaultInactiveAccountAuthenticationHandler(IamProperties properties) { this.iamProps = properties; - WAITING_CONFIRMATION_MSG = String.format("Your registration request to %s was submitted " + waitingConfirmationMsg = String.format("Your registration request to %s was submitted " + "successfully, but you haven't confirmed it yet. Check your inbox, you should have received a message with " + "a confirmation URL", iamProps.getOrganisationName()); - WAITING_APPROVAL_MSG = + waitingApprovalMsg = String.format("Your registration request to %s was submitted and confirmed successfully, " + "and is now waiting for administrator approval. As soon as your request is approved you will receive a " + "confirmation email", iamProps.getOrganisationName()); @@ -43,13 +43,8 @@ public DefaultInactiveAccountAuthenticationHandler(IamProperties properties) { protected boolean hasOngoingRegistrationRequest(IamAccount account) { - if (account.getRegistrationRequest() != null) { - if (ongoingStatus.contains(account.getRegistrationRequest().getStatus())) { - return true; - } - } - - return false; + return (account.getRegistrationRequest() != null + && ongoingStatus.contains(account.getRegistrationRequest().getStatus())); } @@ -81,11 +76,11 @@ public void handleInactiveAccount(IamAccount account) throws UsernameNotFoundExc if (hasOngoingRegistrationRequest(account)) { if (requestWaitingForUserConfirmation(account)) { - raiseAuthenticationError(WAITING_CONFIRMATION_MSG); + raiseAuthenticationError(waitingConfirmationMsg); } if (requestWaitingForAdminApproval(account)) { - raiseAuthenticationError(WAITING_APPROVAL_MSG); + raiseAuthenticationError(waitingApprovalMsg); } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/ExternalAuthenticationFailureHandler.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/ExternalAuthenticationFailureHandler.java index 52c777d57..583a3e2d1 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/ExternalAuthenticationFailureHandler.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/ExternalAuthenticationFailureHandler.java @@ -33,7 +33,7 @@ private String buildRedirectURL(AuthenticationException exception) { try { errorMessage = UriUtils.encode(errorMessage, StandardCharsets.UTF_8.toString()); } catch (UnsupportedEncodingException uex) { - // Unlikely + LOG.error(uex.getMessage(), uex); } return UriComponentsBuilder.fromPath("/login") diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/ExternalAuthenticationHandlerSupport.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/ExternalAuthenticationHandlerSupport.java index 0555f5953..ce2a8a491 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/ExternalAuthenticationHandlerSupport.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/ExternalAuthenticationHandlerSupport.java @@ -1,5 +1,7 @@ package it.infn.mw.iam.authn; +import static it.infn.mw.iam.authn.x509.IamX509PreauthenticationProcessingFilter.X509_CREDENTIAL_SESSION_KEY; + import java.util.Optional; import javax.servlet.http.HttpServletRequest; @@ -15,10 +17,12 @@ import com.google.common.base.Strings; +import it.infn.mw.iam.api.account_linking.AccountLinkingConstants; import it.infn.mw.iam.api.scim.exception.IllegalArgumentException; import it.infn.mw.iam.authn.ExternalAuthenticationRegistrationInfo.ExternalAuthenticationType; +import it.infn.mw.iam.authn.x509.IamX509AuthenticationCredential; -public class ExternalAuthenticationHandlerSupport { +public class ExternalAuthenticationHandlerSupport implements AccountLinkingConstants{ public static final String ACCCOUNT_LINKING_BASE_RESOURCE = "/iam/account-linking"; @@ -59,7 +63,7 @@ protected boolean hasAccountLinkingDoneKey(HttpSession session) { } protected void setAccountLinkingDone(HttpSession session) { - session.setAttribute(ACCOUNT_LINKING_DONE_KEY, "DONE");; + session.setAttribute(ACCOUNT_LINKING_DONE_KEY, "DONE"); } protected boolean isExternalUnregisteredUser(Authentication authentication) { @@ -77,10 +81,23 @@ protected boolean hasOngoingAccountLinking(HttpServletRequest request) { return (request.getSession().getAttribute(ACCOUNT_LINKING_SESSION_KEY) != null); } + protected Optional getSavedX509AuthenticationCredential( + HttpSession session) { + return Optional.ofNullable( + (IamX509AuthenticationCredential) session.getAttribute(X509_CREDENTIAL_SESSION_KEY)); + } + + protected Authentication getAccountLinkingSavedAuthentication(HttpSession session) { return (Authentication) session.getAttribute(ACCOUNT_LINKING_SESSION_SAVED_AUTHENTICATION); } + protected void saveX509LinkingSuccess(IamX509AuthenticationCredential cred, + RedirectAttributes attributes) { + attributes.addFlashAttribute(ACCOUNT_LINKING_DASHBOARD_MESSAGE_KEY, + String.format("Certificate '%s' linked succesfully", cred.getSubject())); + } + protected void saveAccountLinkingSuccess( AbstractExternalAuthenticationToken externalAuthenticationToken, RedirectAttributes attributes) { diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/ExternalAuthenticationInfoProcessor.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/ExternalAuthenticationInfoProcessor.java index 0ba99b305..d0073f214 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/ExternalAuthenticationInfoProcessor.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/ExternalAuthenticationInfoProcessor.java @@ -3,7 +3,7 @@ import java.util.Map; import org.springframework.security.oauth2.provider.OAuth2Authentication; - +@FunctionalInterface public interface ExternalAuthenticationInfoProcessor { Map process(OAuth2Authentication authentication); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/ExternalAuthenticationRegistrationInfo.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/ExternalAuthenticationRegistrationInfo.java index 7dfb64ef3..13f67651f 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/ExternalAuthenticationRegistrationInfo.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/ExternalAuthenticationRegistrationInfo.java @@ -1,9 +1,16 @@ package it.infn.mw.iam.authn; +import java.io.Serializable; + import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -public class ExternalAuthenticationRegistrationInfo { +public class ExternalAuthenticationRegistrationInfo implements Serializable { + + /** + * + */ + private static final long serialVersionUID = 1L; public enum ExternalAuthenticationType { OIDC, SAML @@ -14,6 +21,7 @@ public enum ExternalAuthenticationType { private String issuer; private String subject; + private String subjectAttribute; private String email; @@ -27,6 +35,7 @@ public ExternalAuthenticationRegistrationInfo(ExternalAuthenticationType type) { @JsonCreator public ExternalAuthenticationRegistrationInfo(@JsonProperty("type") String type, @JsonProperty("issuer") String issuer, @JsonProperty("subject") String subject, + @JsonProperty("subject_attribute") String subjectAttribute, @JsonProperty("email") String email, @JsonProperty("given_name") String givenName, @JsonProperty("family_name") String familyName) { @@ -36,6 +45,7 @@ public ExternalAuthenticationRegistrationInfo(@JsonProperty("type") String type, this.email = email; this.givenName = givenName; this.familyName = familyName; + this.subjectAttribute = subjectAttribute; } public ExternalAuthenticationType getType() { @@ -83,4 +93,13 @@ public String getFamilyName() { public void setFamilyName(String familyName) { this.familyName = familyName; } + + @JsonProperty("subject_attribute") + public String getSubjectAttribute() { + return subjectAttribute; + } + + public void setSubjectAttribute(String subjectAttribute) { + this.subjectAttribute = subjectAttribute; + } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/InactiveAccountAuthenticationHander.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/InactiveAccountAuthenticationHander.java index 9d17ac7d9..31e86d036 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/InactiveAccountAuthenticationHander.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/InactiveAccountAuthenticationHander.java @@ -3,7 +3,7 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import it.infn.mw.iam.persistence.model.IamAccount; - +@FunctionalInterface public interface InactiveAccountAuthenticationHander { public void handleInactiveAccount(IamAccount account) throws UsernameNotFoundException; diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/TimestamperSuccessHandler.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/TimestamperSuccessHandler.java index 4928a4b75..1be1869df 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/TimestamperSuccessHandler.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/TimestamperSuccessHandler.java @@ -13,17 +13,23 @@ import org.mitre.openid.connect.web.AuthenticationTimeStamper; import org.slf4j.Logger; import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import it.infn.mw.iam.core.util.IamAuthenticationLogger; +import it.infn.mw.iam.persistence.repository.IamAccountRepository; public class TimestamperSuccessHandler implements AuthenticationSuccessHandler { public static final Logger LOG = getLogger(TimestamperSuccessHandler.class); private final AuthenticationSuccessHandler delegate; - - public TimestamperSuccessHandler(AuthenticationSuccessHandler delegate) { + + private final IamAccountRepository accountRepository; + + public TimestamperSuccessHandler(AuthenticationSuccessHandler delegate, + IamAccountRepository accountRepository) { this.delegate = delegate; + this.accountRepository = accountRepository; } protected void setAuthenticationTimestamp(HttpServletRequest request, @@ -35,13 +41,24 @@ protected void setAuthenticationTimestamp(HttpServletRequest request, IamAuthenticationLogger.INSTANCE.logAuthenticationSuccess(authentication); } + protected void touchLastLoginTimeForIamAccount(Authentication authentication){ + if (authentication instanceof OAuth2Authentication){ + OAuth2Authentication oauth = (OAuth2Authentication) authentication; + if (oauth.getUserAuthentication() != null){ + accountRepository.touchLastLoginTimeForUserWithUsername(oauth.getUserAuthentication().getName()); + } + }else { + accountRepository.touchLastLoginTimeForUserWithUsername(authentication.getName()); + } + } + @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { delegate.onAuthenticationSuccess(request, response, authentication); setAuthenticationTimestamp(request, response, authentication); + touchLastLoginTimeForIamAccount(authentication); } - } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/DefaultOidcTokenRequestor.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/DefaultOidcTokenRequestor.java index 414df00ec..16db15496 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/DefaultOidcTokenRequestor.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/DefaultOidcTokenRequestor.java @@ -73,40 +73,39 @@ protected HttpEntity> prepareTokenRequest( switch (config.clientConfig.getTokenEndpointAuthMethod()) { case SECRET_BASIC: - basicAuthRequest(config.clientConfig, headers); - break; + basicAuthRequest(config.clientConfig, headers); + break; case SECRET_JWT: - jwtAuthRequest(config.clientConfig); - break; + jwtAuthRequest(config.clientConfig); + break; case PRIVATE_KEY: - jwtPrivateKeyAuthRequest(config.clientConfig); - break; + jwtPrivateKeyAuthRequest(config.clientConfig); + break; case SECRET_POST: - formAuthRequest(config.clientConfig, tokenRequestParams); - break; + formAuthRequest(config.clientConfig, tokenRequestParams); + break; case NONE: - break; + break; default: - throw new AuthenticationServiceException( - "Unsupported token endpoint authentication method"); + throw new AuthenticationServiceException( + "Unsupported token endpoint authentication method"); } - HttpEntity> tokenRequest = - new HttpEntity<>(tokenRequestParams, headers); + return + new HttpEntity<>(tokenRequestParams, headers); - return tokenRequest; } Optional parseErrorResponse(HttpClientErrorException e) { try { TokenEndpointErrorResponse response = jacksonObjectMapper - .readValue(e.getResponseBodyAsByteArray(), TokenEndpointErrorResponse.class); + .readValue(e.getResponseBodyAsByteArray(), TokenEndpointErrorResponse.class); return Optional.of(response); } catch (Exception jsonParsingError) { LOG.error("Error parsing token endpoint response: {}. input: {}", - jsonParsingError.getMessage(), e.getResponseBodyAsString()); + jsonParsingError.getMessage(), e.getResponseBodyAsString(), jsonParsingError); return Optional.empty(); } @@ -122,28 +121,28 @@ public String requestTokens(OidcProviderConfiguration conf, try { return restTemplate.postForObject(conf.serverConfig.getTokenEndpointUri(), - prepareTokenRequest(conf, tokenRequestParams), String.class); + prepareTokenRequest(conf, tokenRequestParams), String.class); } catch (HttpClientErrorException e) { if (e.getStatusCode() != null && e.getStatusCode().equals(BAD_REQUEST)) { - parseErrorResponse(e).ifPresent(er -> { + parseErrorResponse(e).ifPresent(er -> { - String errorMessage = String.format("Token request error: %s '%s'", er.getError(), - er.getErrorDescription()); - LOG.error(errorMessage); + String errorMessage = String.format("Token request error: %s '%s'", er.getError(), + er.getErrorDescription()); + LOG.error(errorMessage); - throw new OidcClientError(e.getMessage(), er.getError(), er.getErrorDescription(), - er.getErrorUri()); + throw new OidcClientError(e.getMessage(), er.getError(), er.getErrorDescription(), + er.getErrorUri()); - }); + }); } String errorMessage = String.format("Token request error: %s", e.getMessage()); LOG.error(errorMessage, e); throw new OidcClientError(errorMessage, e); - } catch (Throwable e) { + } catch (Exception e) { String errorMessage = String.format("Token request error: %s", e.getMessage()); LOG.error(errorMessage, e); throw new OidcClientError(errorMessage, e); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/OidcClientFilter.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/OidcClientFilter.java index 724029f63..d6e7fd9a2 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/OidcClientFilter.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/OidcClientFilter.java @@ -35,9 +35,9 @@ import com.nimbusds.jwt.SignedJWT; /** - * A slightly modified version of mitreid client filter that allows to provide a custom {@link - * ClientHttpRequestFactory} object. This is needed to accomodate SSL connections to providers that - * use EUGridPMA certificates. + * A slightly modified version of mitreid client filter that allows to provide a custom + * {@link ClientHttpRequestFactory} object. This is needed to accomodate SSL connections to + * providers that use EUGridPMA certificates. * */ public class OidcClientFilter extends OIDCAuthenticationFilter { @@ -51,7 +51,7 @@ public OidcProviderConfiguration(ServerConfiguration sc, RegisteredClient cc) { ServerConfiguration serverConfig; RegisteredClient clientConfig; - }; + } public static final Logger LOG = LoggerFactory.getLogger(OidcClientFilter.class); @@ -82,18 +82,18 @@ protected OidcProviderConfiguration lookupProvider(HttpServletRequest request) { throw new AuthenticationServiceException("Issuser not found in session."); } ServerConfiguration serverConfig = - getServerConfigurationService().getServerConfiguration(issuer); + getServerConfigurationService().getServerConfiguration(issuer); if (serverConfig == null) { throw new AuthenticationServiceException("Unknow OpenID provider :" + issuer); } RegisteredClient clientConfig = - getClientConfigurationService().getClientConfiguration(serverConfig); + getClientConfigurationService().getClientConfiguration(serverConfig); if (clientConfig == null) { throw new AuthenticationServiceException( - "Client configuration not found for OpenID provider :" + issuer); + "Client configuration not found for OpenID provider :" + issuer); } return new OidcProviderConfiguration(serverConfig, clientConfig); @@ -145,11 +145,9 @@ private JWT parseToken(String tokenValue) { protected void handleError(HttpServletRequest request, HttpServletResponse response) throws IOException { - OidcClientError error = - new OidcClientError("External authentication error", request.getParameter("error"), - request.getParameter("error_description"), request.getParameter("error_uri")); + throw new OidcClientError("External authentication error", request.getParameter("error"), + request.getParameter("error_description"), request.getParameter("error_uri")); - throw error; } @Override @@ -164,11 +162,11 @@ protected Authentication handleAuthorizationCodeResponse(HttpServletRequest requ try { tokenResponseString = - tokenRequestor.requestTokens(config, initTokenRequestParameters(request, config)); + tokenRequestor.requestTokens(config, initTokenRequestParameters(request, config)); } catch (OidcClientError e) { LOG.error("Error executing token request against endpoint {}: {}", - config.serverConfig.getTokenEndpointUri(), e.getMessage(), e); + config.serverConfig.getTokenEndpointUri(), e.getMessage(), e); throw e; } @@ -184,7 +182,7 @@ protected Authentication handleAuthorizationCodeResponse(HttpServletRequest requ accessTokenValue = tokenResponse.get("access_token").getAsString(); } else { throw new AuthenticationServiceException( - "Token Endpoint did not return an access_token. Response: " + tokenResponseString); + "Token Endpoint did not return an access_token. Response: " + tokenResponseString); } if (tokenResponse.has("id_token")) { @@ -230,11 +228,9 @@ protected void validateSignature(JWT idToken, OidcProviderConfiguration config) JWTSigningAndValidationService jwtValidator = null; - if (clientAlg != null) { - if (!clientAlg.equals(tokenAlg)) { - throw new AuthenticationServiceException( - "Token algorithm " + tokenAlg + " does not match expected algorithm " + clientAlg); - } + if (clientAlg != null && !clientAlg.equals(tokenAlg)) { + throw new AuthenticationServiceException( + "Token algorithm " + tokenAlg + " does not match expected algorithm " + clientAlg); } if (idToken instanceof PlainJWT) { @@ -370,11 +366,13 @@ protected void validateClaims(HttpSession session, JWT idToken, JWTClaimsSet idC } } + @Override public int getTimeSkewAllowance() { return timeSkewAllowance; } + @Override public void setTimeSkewAllowance(int timeSkewAllowance) { this.timeSkewAllowance = timeSkewAllowance; diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/OidcExceptionMessageHelper.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/OidcExceptionMessageHelper.java index 5cd5ced30..647441807 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/OidcExceptionMessageHelper.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/OidcExceptionMessageHelper.java @@ -10,7 +10,7 @@ public String buildErrorMessage(AuthenticationException e) { if (e instanceof OidcClientError) { OidcClientError error = (OidcClientError) e; - if (error.getError().equals("access_denied")) { + if ("access_denied".equals(error.getError())) { return "User denied access to requested identity information"; } return error.getError(); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/OidcExternalAuthenticationToken.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/OidcExternalAuthenticationToken.java index 7fad8edd8..4b76dbcd0 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/OidcExternalAuthenticationToken.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/OidcExternalAuthenticationToken.java @@ -36,7 +36,7 @@ public Map buildAuthnInfoMap(ExternalAuthenticationInfoBuilder v } @Override - public ExternalAuthenticationRegistrationInfo toExernalAuthenticationInfo() { + public ExternalAuthenticationRegistrationInfo toExernalAuthenticationRegistrationInfo() { ExternalAuthenticationRegistrationInfo ri = new ExternalAuthenticationRegistrationInfo(ExternalAuthenticationType.OIDC); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/OidcTokenRequestor.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/OidcTokenRequestor.java index 8df5bcf58..9096aa920 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/OidcTokenRequestor.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/OidcTokenRequestor.java @@ -3,7 +3,7 @@ import org.springframework.util.MultiValueMap; import it.infn.mw.iam.authn.oidc.OidcClientFilter.OidcProviderConfiguration; - +@FunctionalInterface public interface OidcTokenRequestor { String requestTokens(OidcProviderConfiguration conf, diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/RestTemplateFactory.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/RestTemplateFactory.java index ffa75dd65..d6ece1b1d 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/RestTemplateFactory.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/RestTemplateFactory.java @@ -1,7 +1,7 @@ package it.infn.mw.iam.authn.oidc; import org.springframework.web.client.RestTemplate; - +@FunctionalInterface public interface RestTemplateFactory { RestTemplate newRestTemplate(); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/service/DefaultOidcUserDetailsService.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/service/DefaultOidcUserDetailsService.java index 6e25efbee..5fbf18c30 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/service/DefaultOidcUserDetailsService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/service/DefaultOidcUserDetailsService.java @@ -55,10 +55,8 @@ protected User buildUserFromIamAccount(IamAccount account) { protected User buildUserFromOIDCAuthentication(OIDCAuthenticationToken token) { String username = token.getSub(); - if (token.getUserInfo() != null) { - if (!Strings.isNullOrEmpty(token.getUserInfo().getName())) { - username = token.getUserInfo().getName(); - } + if (token.getUserInfo() != null && !Strings.isNullOrEmpty(token.getUserInfo().getName())) { + username = token.getUserInfo().getName(); } return new User(username, "", diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/service/OidcUserDetailsService.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/service/OidcUserDetailsService.java index 1a4ce77e4..a26d3373b 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/service/OidcUserDetailsService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/oidc/service/OidcUserDetailsService.java @@ -1,7 +1,7 @@ package it.infn.mw.iam.authn.oidc.service; import org.mitre.openid.connect.model.OIDCAuthenticationToken; - +@FunctionalInterface public interface OidcUserDetailsService { Object loadUserByOIDC(OIDCAuthenticationToken token); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/CleanInactiveProvisionedAccounts.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/CleanInactiveProvisionedAccounts.java new file mode 100644 index 000000000..a16f1b172 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/CleanInactiveProvisionedAccounts.java @@ -0,0 +1,61 @@ +package it.infn.mw.iam.authn.saml; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import it.infn.mw.iam.core.time.TimeProvider; +import it.infn.mw.iam.core.user.IamAccountService; +import it.infn.mw.iam.persistence.model.IamAccount; + +public class CleanInactiveProvisionedAccounts implements Runnable { + + public static final Logger LOG = LoggerFactory.getLogger(CleanInactiveProvisionedAccounts.class); + + final TimeProvider timeProvider; + final IamAccountService accountService; + final int inactiveUserLifetimeInDays; + + public CleanInactiveProvisionedAccounts(TimeProvider timeProvider, + IamAccountService accountService, int inactiveUserLifetimeInDays) { + checkNotNull(timeProvider, "null timeProvider"); + checkNotNull(accountService, "null accountService"); + checkArgument(inactiveUserLifetimeInDays > 0, "inactiveUserLifetimeInDays must be > 0"); + this.timeProvider = timeProvider; + this.accountService = accountService; + this.inactiveUserLifetimeInDays = inactiveUserLifetimeInDays; + } + + private Date computeProvisionedUsersExpirationTimestamp() { + LocalDateTime ldt = LocalDateTime + .ofInstant(Instant.ofEpochMilli(timeProvider.currentTimeMillis()), ZoneId.systemDefault()); + + return Date + .from(ldt.minusDays(inactiveUserLifetimeInDays).atZone(ZoneId.systemDefault()).toInstant()); + } + + @Override + public void run() { + Date expirationTimestamp = computeProvisionedUsersExpirationTimestamp(); + + LOG.info("Attempting removal of provisioned accounts inactive since {}", expirationTimestamp); + + List removedAccounts = + accountService.deleteInactiveProvisionedUsersSinceTime(expirationTimestamp); + + if (removedAccounts.isEmpty()) { + LOG.info("No accounts removed"); + } else { + removedAccounts.forEach(a -> LOG.info("Removed inactive provisioned account: {}", a)); + } + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/DefaultMetadataLookupService.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/DefaultMetadataLookupService.java index f1db4bd9e..969238864 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/DefaultMetadataLookupService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/DefaultMetadataLookupService.java @@ -4,7 +4,10 @@ import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; import org.opensaml.common.xml.SAMLConstants; @@ -34,23 +37,36 @@ public class DefaultMetadataLookupService implements MetadataLookupService { private static final Logger LOG = LoggerFactory.getLogger(DefaultMetadataLookupService.class); Set descriptions = new HashSet<>(); + ReadWriteLock lock = new ReentrantReadWriteLock(true); MetadataManager metadataManager; @Autowired public DefaultMetadataLookupService(MetadataManager manager) { this.metadataManager = manager; - try { - initializeMetadataSet(); - } catch (MetadataProviderException e) { - throw new IllegalStateException(e); - } + refreshMetadata(); } private void initializeMetadataSet() throws MetadataProviderException { + LOG.info("Initializing IdP descriptor list from metadata"); + + Set newDescriptions = new HashSet<>(); + for (String idpName : metadataManager.getIDPEntityNames()) { - descriptions.add(descriptionFromMetadata(metadataManager.getEntityDescriptor(idpName))); + + IdpDescription idpDescription = + descriptionFromMetadata(metadataManager.getEntityDescriptor(idpName)); + + LOG.debug("Adding IdP description: {}", idpDescription); + newDescriptions.add(idpDescription); + } + + try { + lock.writeLock().lock(); + descriptions = newDescriptions; + } finally { + lock.writeLock().unlock(); } } @@ -60,32 +76,26 @@ private IdpDescription descriptionFromMetadata(EntityDescriptor descriptor) { result.setEntityId(descriptor.getEntityID()); IDPSSODescriptor idpDesc = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS); - if (idpDesc != null) { - if (idpDesc.getExtensions() != null) { - - for (final XMLObject object : idpDesc.getExtensions() - .getUnknownXMLObjects(UIInfo.DEFAULT_ELEMENT_NAME)) { - if (object instanceof UIInfo) { - UIInfo uiInfo = (UIInfo) object; - - if (!uiInfo.getDisplayNames().isEmpty()) { - result - .setOrganizationName(uiInfo.getDisplayNames().get(0).getName().getLocalString()); - } + if (idpDesc != null && idpDesc.getExtensions() != null) { - if (!uiInfo.getLogos().isEmpty()) { + for (final XMLObject object : idpDesc.getExtensions() + .getUnknownXMLObjects(UIInfo.DEFAULT_ELEMENT_NAME)) { + if (object instanceof UIInfo) { + UIInfo uiInfo = (UIInfo) object; - Logo minLogo = - uiInfo.getLogos().stream().min(Comparator.comparing(Logo::getHeight)).get(); + if (!uiInfo.getDisplayNames().isEmpty()) { + result.setOrganizationName(uiInfo.getDisplayNames().get(0).getName().getLocalString()); + } - result.setImageUrl(minLogo.getURL()); - } + if (!uiInfo.getLogos().isEmpty()) { + uiInfo.getLogos().stream().min(Comparator.comparing(Logo::getHeight)).ifPresent( + l -> result.setImageUrl(l.getURL())); } } } } - if (result.getOrganizationName() == null || result.getOrganizationName().isEmpty()) { + if (Strings.isNullOrEmpty(result.getOrganizationName())) { result.setOrganizationName(result.getEntityId()); } @@ -93,28 +103,42 @@ private IdpDescription descriptionFromMetadata(EntityDescriptor descriptor) { } - - @Override - public List lookupIdp(String text) { - + private Optional> lookupByEntityId(String text) { // Try entityId match try { EntityDescriptor entityDescriptor = metadataManager.getEntityDescriptor(text); if (entityDescriptor != null) { - return ImmutableList.of(descriptionFromMetadata(entityDescriptor)); + return Optional.of(ImmutableList.of(descriptionFromMetadata(entityDescriptor))); } } catch (MetadataProviderException e) { - throw new RuntimeException(e); + throw new SamlMetadataError(e.getMessage(), e); } - return descriptions.stream() - .filter(p -> p.getOrganizationName() != null - && p.getOrganizationName().toLowerCase().contains(text.toLowerCase())) - .limit(MAX_RESULTS) - .collect(Collectors.toList()); + return Optional.empty(); + } + + @Override + public List lookupIdp(String text) { + + List result = new ArrayList<>(); + + lookupByEntityId(text).ifPresent(result::addAll); + + if (!result.isEmpty()) { + return result; + } + try { + lock.readLock().lock(); + return descriptions.stream() + .filter(p -> p.getOrganizationName().toLowerCase().contains(text.toLowerCase())) + .limit(MAX_RESULTS) + .collect(Collectors.toList()); + } finally { + lock.readLock().unlock(); + } } @Override @@ -142,5 +166,13 @@ public List listIdps() { } + @Override + public void refreshMetadata() { + try { + initializeMetadataSet(); + } catch (MetadataProviderException e) { + throw new SamlMetadataError(e); + } + } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/DefaultSAMLUserDetailsService.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/DefaultSAMLUserDetailsService.java index 1dcd1ae50..151289ab8 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/DefaultSAMLUserDetailsService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/DefaultSAMLUserDetailsService.java @@ -1,76 +1,42 @@ package it.infn.mw.iam.authn.saml; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.saml.SAMLCredential; import org.springframework.security.saml.userdetails.SAMLUserDetailsService; -import it.infn.mw.iam.authn.ExternalAuthenticationHandlerSupport; import it.infn.mw.iam.authn.InactiveAccountAuthenticationHander; import it.infn.mw.iam.authn.saml.util.SamlUserIdentifierResolver; import it.infn.mw.iam.persistence.model.IamAccount; -import it.infn.mw.iam.persistence.model.IamAuthority; +import it.infn.mw.iam.persistence.model.IamSamlId; import it.infn.mw.iam.persistence.repository.IamAccountRepository; -public class DefaultSAMLUserDetailsService implements SAMLUserDetailsService { +public class DefaultSAMLUserDetailsService extends SAMLUserDetailsServiceSupport + implements SAMLUserDetailsService { - final SamlUserIdentifierResolver resolver; final IamAccountRepository repo; - final InactiveAccountAuthenticationHander inactiveAccountHandler; - @Autowired public DefaultSAMLUserDetailsService(SamlUserIdentifierResolver resolver, IamAccountRepository repo, InactiveAccountAuthenticationHander handler) { - this.resolver = resolver; + super(handler, resolver); this.repo = repo; - this.inactiveAccountHandler = handler; - } - - - List convertAuthorities(IamAccount a) { - - List authorities = new ArrayList<>(); - for (IamAuthority auth : a.getAuthorities()) { - authorities.add(new SimpleGrantedAuthority(auth.getAuthority())); - } - return authorities; - } - - protected User buildUserFromIamAccount(IamAccount account) { - inactiveAccountHandler.handleInactiveAccount(account); - return new User(account.getUsername(), account.getPassword(), convertAuthorities(account)); - } - - protected User buildUserFromSamlCredential(String userSamlId, SAMLCredential credential) { - return new User(userSamlId, "", - Arrays.asList(ExternalAuthenticationHandlerSupport.EXT_AUTHN_UNREGISTERED_USER_AUTH)); } @Override public Object loadUserBySAML(SAMLCredential credential) throws UsernameNotFoundException { - String issuerId = credential.getRemoteEntityID(); - - String userSamlId = - resolver.getUserIdentifier(credential).orElseThrow(() -> new UsernameNotFoundException( - "Could not extract a user identifier from the SAML assertion")); + IamSamlId samlId = resolverSamlId(credential); - Optional account = repo.findBySamlId(issuerId, userSamlId); + Optional account = repo.findBySamlId(samlId); if (account.isPresent()) { return buildUserFromIamAccount(account.get()); } - return buildUserFromSamlCredential(userSamlId, credential); + return buildUserFromSamlCredential(samlId, credential); } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/IamSamlAuthenticationProvider.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/IamSamlAuthenticationProvider.java index e0d3bbb66..88dfe2903 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/IamSamlAuthenticationProvider.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/IamSamlAuthenticationProvider.java @@ -1,13 +1,40 @@ package it.infn.mw.iam.authn.saml; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.User; import org.springframework.security.providers.ExpiringUsernameAuthenticationToken; import org.springframework.security.saml.SAMLAuthenticationProvider; +import org.springframework.security.saml.SAMLCredential; + +import com.google.common.base.Joiner; + +import it.infn.mw.iam.authn.saml.util.SamlUserIdentifierResolutionResult; +import it.infn.mw.iam.authn.saml.util.SamlUserIdentifierResolver; +import it.infn.mw.iam.persistence.model.IamSamlId; public class IamSamlAuthenticationProvider extends SAMLAuthenticationProvider { + final SamlUserIdentifierResolver userIdResolver; + final Joiner joiner = Joiner.on(",").skipNulls(); + + public IamSamlAuthenticationProvider(SamlUserIdentifierResolver resolver) { + this.userIdResolver = resolver; + } + + private Supplier handleResolutionFailure( + SamlUserIdentifierResolutionResult result) { + + List errorMessages = result.getErrorMessages().orElse(Collections.emptyList()); + + return () -> new AuthenticationServiceException(joiner.join(errorMessages)); + } + @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { @@ -20,11 +47,16 @@ public Authentication authenticate(Authentication authentication) throws Authent User user = (User) token.getDetails(); - SamlExternalAuthenticationToken extAuthnToken = - new SamlExternalAuthenticationToken(token, token.getTokenExpiration(), user.getUsername(), - token.getCredentials(), token.getAuthorities()); + SAMLCredential samlCredentials = (SAMLCredential) token.getCredentials(); + + SamlUserIdentifierResolutionResult result = + userIdResolver.resolveSamlUserIdentifier(samlCredentials); + + IamSamlId samlId = result.getResolvedId().orElseThrow(handleResolutionFailure(result)); + + return new SamlExternalAuthenticationToken(samlId, token, token.getTokenExpiration(), + user.getUsername(), token.getCredentials(), token.getAuthorities()); - return extAuthnToken; } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/JustInTimeProvisioningSAMLUserDetailsService.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/JustInTimeProvisioningSAMLUserDetailsService.java new file mode 100644 index 000000000..abc814ab7 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/JustInTimeProvisioningSAMLUserDetailsService.java @@ -0,0 +1,114 @@ +package it.infn.mw.iam.authn.saml; + +import static com.google.common.base.Preconditions.checkNotNull; +import static it.infn.mw.iam.authn.saml.util.Saml2Attribute.GIVEN_NAME; +import static it.infn.mw.iam.authn.saml.util.Saml2Attribute.MAIL; + +import java.util.EnumSet; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.saml.SAMLCredential; +import org.springframework.security.saml.userdetails.SAMLUserDetailsService; + +import it.infn.mw.iam.authn.InactiveAccountAuthenticationHander; +import it.infn.mw.iam.authn.saml.util.Saml2Attribute; +import it.infn.mw.iam.authn.saml.util.SamlUserIdentifierResolver; +import it.infn.mw.iam.core.user.IamAccountService; +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamSamlId; +import it.infn.mw.iam.persistence.repository.IamAccountRepository; + +public class JustInTimeProvisioningSAMLUserDetailsService extends SAMLUserDetailsServiceSupport + implements SAMLUserDetailsService { + + private final IamAccountRepository repo; + private final IamAccountService accountService; + private final Optional> trustedIdpEntityIds; + + private static final EnumSet REQUIRED_SAML_ATTRIBUTES = + EnumSet.of(Saml2Attribute.MAIL, Saml2Attribute.GIVEN_NAME, Saml2Attribute.SN); + + public JustInTimeProvisioningSAMLUserDetailsService(SamlUserIdentifierResolver resolver, + IamAccountService accountService, InactiveAccountAuthenticationHander inactiveAccountHandler, + IamAccountRepository repo, + Optional> trustedIdpEntityIds) { + super(inactiveAccountHandler, resolver); + this.accountService = accountService; + this.repo = repo; + this.trustedIdpEntityIds = trustedIdpEntityIds; + } + + + protected void samlCredentialEntityIdChecks(SAMLCredential credential){ + trustedIdpEntityIds.ifPresent(l -> { + if (!l.contains(credential.getRemoteEntityID())){ + throw new UsernameNotFoundException( + String.format("Error provisioning user! SAML credential issuer '%s' is not trusted" + + " for just-in-time account provisioning.", credential.getRemoteEntityID())); + } + }); + } + + protected void samlCredentialSanityChecks(SAMLCredential credential) { + + for (Saml2Attribute a : REQUIRED_SAML_ATTRIBUTES) { + if (credential.getAttributeAsString(a.getAttributeName()) == null) { + throw new UsernameNotFoundException(String.format( + "Error provisioning user! SAML credential is missing required attribute: %s (%s)", + a.getAlias(), a.getAttributeName())); + } + } + + + } + + + @Override + public Object loadUserBySAML(SAMLCredential credential) throws UsernameNotFoundException { + checkNotNull(credential, "null saml credential"); + + IamSamlId samlId = resolverSamlId(credential); + + Optional account = repo.findBySamlId(samlId); + + if (account.isPresent()) { + return buildUserFromIamAccount(account.get()); + } + + samlCredentialEntityIdChecks(credential); + samlCredentialSanityChecks(credential); + + final String randomUuid = UUID.randomUUID().toString(); + + // Create account from SAMLCredential + IamAccount newAccount = IamAccount.newAccount(); + + newAccount.setProvisioned(true); + newAccount.getSamlIds().add(samlId); + samlId.setAccount(newAccount); + + newAccount.setActive(true); + + if (samlId.getUserId().length() < 128) { + newAccount.setUsername(samlId.getUserId()); + } else { + newAccount.setUsername(randomUuid); + } + + newAccount.getUserInfo() + .setGivenName(credential.getAttributeAsString(GIVEN_NAME.getAttributeName())); + + newAccount.getUserInfo() + .setFamilyName(credential.getAttributeAsString(Saml2Attribute.SN.getAttributeName())); + + newAccount.getUserInfo().setEmail(credential.getAttributeAsString(MAIL.getAttributeName())); + + accountService.createAccount(newAccount); + + return buildUserFromIamAccount(newAccount); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/MetadataLookupService.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/MetadataLookupService.java index 29dc82ff1..f28b8900d 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/MetadataLookupService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/MetadataLookupService.java @@ -9,4 +9,6 @@ public interface MetadataLookupService { List lookupIdp(String text); List listIdps(); + + void refreshMetadata(); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/SAMLUserDetailsServiceSupport.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/SAMLUserDetailsServiceSupport.java new file mode 100644 index 000000000..8d84811e9 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/SAMLUserDetailsServiceSupport.java @@ -0,0 +1,56 @@ +package it.infn.mw.iam.authn.saml; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.saml.SAMLCredential; + +import it.infn.mw.iam.authn.ExternalAuthenticationHandlerSupport; +import it.infn.mw.iam.authn.InactiveAccountAuthenticationHander; +import it.infn.mw.iam.authn.saml.util.SamlUserIdentifierResolver; +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamAuthority; +import it.infn.mw.iam.persistence.model.IamSamlId; + +public class SAMLUserDetailsServiceSupport { + + private final InactiveAccountAuthenticationHander inactiveAccountHandler; + private final SamlUserIdentifierResolver resolver; + + protected SAMLUserDetailsServiceSupport(InactiveAccountAuthenticationHander inactiveAccountHandler, + SamlUserIdentifierResolver resolver) { + + this.inactiveAccountHandler = inactiveAccountHandler; + this.resolver = resolver; + } + + protected List convertAuthorities(IamAccount a) { + + List authorities = new ArrayList<>(); + for (IamAuthority auth : a.getAuthorities()) { + authorities.add(new SimpleGrantedAuthority(auth.getAuthority())); + } + return authorities; + } + + protected User buildUserFromIamAccount(IamAccount account) { + inactiveAccountHandler.handleInactiveAccount(account); + return new User(account.getUsername(), account.getPassword(), convertAuthorities(account)); + } + + protected User buildUserFromSamlCredential(IamSamlId samlId, SAMLCredential credential) { + return new User(samlId.getUserId(), "", + Arrays.asList(ExternalAuthenticationHandlerSupport.EXT_AUTHN_UNREGISTERED_USER_AUTH)); + } + + protected IamSamlId resolverSamlId(SAMLCredential credential) { + return resolver.resolveSamlUserIdentifier(credential).getResolvedId() + .orElseThrow(() -> new UsernameNotFoundException( + "Could not extract a user identifier from the SAML assertion")); + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/SamlExternalAuthenticationToken.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/SamlExternalAuthenticationToken.java index b964522e2..a56addef4 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/SamlExternalAuthenticationToken.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/SamlExternalAuthenticationToken.java @@ -1,5 +1,9 @@ package it.infn.mw.iam.authn.saml; +import static it.infn.mw.iam.authn.saml.util.Saml2Attribute.GIVEN_NAME; +import static it.infn.mw.iam.authn.saml.util.Saml2Attribute.MAIL; +import static it.infn.mw.iam.authn.saml.util.Saml2Attribute.SN; + import java.util.Collection; import java.util.Date; import java.util.Map; @@ -16,21 +20,20 @@ import it.infn.mw.iam.authn.ExternalAuthenticationRegistrationInfo; import it.infn.mw.iam.authn.ExternalAuthenticationRegistrationInfo.ExternalAuthenticationType; import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamSamlId; public class SamlExternalAuthenticationToken extends AbstractExternalAuthenticationToken { private static final long serialVersionUID = -7854473523011856692L; - - public static final String OID_GIVEN_NAME = "urn:oid:2.5.4.42"; - public static final String OID_SN = "urn:oid:2.5.4.4"; - public static final String OID_CN = "urn:oid:2.5.4.3"; - public static final String OID_MAIL = "urn:oid:0.9.2342.19200300.100.1.3"; - - public SamlExternalAuthenticationToken(ExpiringUsernameAuthenticationToken authn, + + private final IamSamlId samlId; + + public SamlExternalAuthenticationToken(IamSamlId samlId, ExpiringUsernameAuthenticationToken authn, Date tokenExpiration, Object principal, Object credentials, Collection authorities) { super(authn, tokenExpiration, principal, credentials, authorities); + this.samlId = samlId; } @Override @@ -40,26 +43,27 @@ public Map buildAuthnInfoMap(ExternalAuthenticationInfoBuilder v } @Override - public ExternalAuthenticationRegistrationInfo toExernalAuthenticationInfo() { + public ExternalAuthenticationRegistrationInfo toExernalAuthenticationRegistrationInfo() { ExternalAuthenticationRegistrationInfo ri = new ExternalAuthenticationRegistrationInfo(ExternalAuthenticationType.SAML); SAMLCredential cred = (SAMLCredential) getExternalAuthentication().getCredentials(); - ri.setIssuer(cred.getRemoteEntityID()); - ri.setSubject(getName()); + ri.setIssuer(samlId.getIdpId()); + ri.setSubject(samlId.getUserId()); + ri.setSubjectAttribute(samlId.getAttributeId()); - if (!Strings.isNullOrEmpty(cred.getAttributeAsString(OID_GIVEN_NAME))) { - ri.setGivenName(cred.getAttributeAsString(OID_GIVEN_NAME)); + if (!Strings.isNullOrEmpty(cred.getAttributeAsString(GIVEN_NAME.getAttributeName()))) { + ri.setGivenName(cred.getAttributeAsString(GIVEN_NAME.getAttributeName())); } - if (!Strings.isNullOrEmpty(cred.getAttributeAsString(OID_SN))) { - ri.setFamilyName(cred.getAttributeAsString(OID_SN)); + if (!Strings.isNullOrEmpty(cred.getAttributeAsString(SN.getAttributeName()))) { + ri.setFamilyName(cred.getAttributeAsString(SN.getAttributeName())); } - if (!Strings.isNullOrEmpty(cred.getAttributeAsString(OID_MAIL))) { - ri.setEmail(cred.getAttributeAsString(OID_MAIL)); + if (!Strings.isNullOrEmpty(cred.getAttributeAsString(MAIL.getAttributeName()))) { + ri.setEmail(cred.getAttributeAsString(MAIL.getAttributeName())); } return ri; @@ -70,4 +74,7 @@ public void linkToIamAccount(ExternalAccountLinker visitor, IamAccount account) visitor.linkToIamAccount(account, this); } + public IamSamlId getSamlId() { + return samlId; + } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/SamlMetadataError.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/SamlMetadataError.java new file mode 100644 index 000000000..77188dd02 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/SamlMetadataError.java @@ -0,0 +1,23 @@ +package it.infn.mw.iam.authn.saml; + +public class SamlMetadataError extends RuntimeException { + + + /** + * + */ + private static final long serialVersionUID = 1L; + + public SamlMetadataError(String message) { + super(message); + } + + public SamlMetadataError(Throwable cause) { + super(cause); + } + + public SamlMetadataError(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/model/IdpDescription.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/model/IdpDescription.java index 8f0ae0751..dc778f5b2 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/model/IdpDescription.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/model/IdpDescription.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; @JsonInclude(Include.NON_EMPTY) -public class IdpDescription { +public class IdpDescription { private String entityId; private String organizationName; @@ -34,5 +34,9 @@ public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; } - + @Override + public String toString() { + return "IdpDescription [entityId=" + entityId + ", organizationName=" + organizationName + + ", imageUrl=" + imageUrl + "]"; + } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/AbstractSamlUserIdentifierResolver.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/AbstractSamlUserIdentifierResolver.java new file mode 100644 index 000000000..d46d9a093 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/AbstractSamlUserIdentifierResolver.java @@ -0,0 +1,17 @@ +package it.infn.mw.iam.authn.saml.util; + +public abstract class AbstractSamlUserIdentifierResolver + implements NamedSamlUserIdentifierResolver { + + private final String name; + + public AbstractSamlUserIdentifierResolver(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/AttributeUserIdentifierResolver.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/AttributeUserIdentifierResolver.java index 171747ed0..42a84f102 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/AttributeUserIdentifierResolver.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/AttributeUserIdentifierResolver.java @@ -1,22 +1,37 @@ package it.infn.mw.iam.authn.saml.util; -import java.util.Optional; - import org.springframework.security.saml.SAMLCredential; -public class AttributeUserIdentifierResolver implements SamlUserIdentifierResolver { +import it.infn.mw.iam.persistence.model.IamSamlId; + +public class AttributeUserIdentifierResolver extends AbstractSamlUserIdentifierResolver { - private final String attributeName; + Saml2Attribute attribute; - public AttributeUserIdentifierResolver(String attributeName) { - this.attributeName = attributeName; + public AttributeUserIdentifierResolver(Saml2Attribute attribute) { + super(attribute.name()); + this.attribute = attribute; } @Override - public Optional getUserIdentifier(SAMLCredential samlCredential) { - - return Optional.ofNullable(samlCredential.getAttributeAsString(attributeName)); - + public SamlUserIdentifierResolutionResult resolveSamlUserIdentifier( + SAMLCredential samlCredential) { + + String attributeValue = samlCredential.getAttributeAsString(attribute.getAttributeName()); + + if (attributeValue == null) { + return SamlUserIdentifierResolutionResult + .resolutionFailure(String.format("Attribute '%s:%s' not found in assertion", attribute.getAlias(), + attribute.getAttributeName())); + } + + IamSamlId samlId = new IamSamlId(); + samlId.setIdpId(samlCredential.getRemoteEntityID()); + samlId.setAttributeId(attribute.getAttributeName()); + samlId.setUserId(attributeValue); + + return SamlUserIdentifierResolutionResult.resolutionSuccess(samlId); + } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/FirstApplicableChainedSamlIdResolver.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/FirstApplicableChainedSamlIdResolver.java index 3a7db146e..115c21acd 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/FirstApplicableChainedSamlIdResolver.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/FirstApplicableChainedSamlIdResolver.java @@ -1,7 +1,7 @@ package it.infn.mw.iam.authn.saml.util; +import java.util.ArrayList; import java.util.List; -import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,25 +18,29 @@ public FirstApplicableChainedSamlIdResolver(List res } @Override - public Optional getUserIdentifier(SAMLCredential samlCredential) { + public SamlUserIdentifierResolutionResult resolveSamlUserIdentifier(SAMLCredential samlCredential) { + List errorMessages = new ArrayList<>(); + for (SamlUserIdentifierResolver resolver : resolvers) { LOG.debug("Attempting SAML user id resolution with resolver {}", resolver.getClass().getName()); - Optional userId = resolver.getUserIdentifier(samlCredential); - - if (userId.isPresent()) { - - LOG.debug("Resolved user id: {}", userId); - return userId; + SamlUserIdentifierResolutionResult result = resolver + .resolveSamlUserIdentifier(samlCredential); + + if (result.getResolvedId().isPresent()){ + LOG.debug("Resolved SAML user id: {}", result.getResolvedId().get()); + return result; } + + result.getErrorMessages().ifPresent(errorMessages::addAll); } LOG.debug( "All configured user id resolvers could not resolve the user id from SAML credential"); - return Optional.empty(); + return SamlUserIdentifierResolutionResult.resolutionFailure(errorMessages); } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/NameIdUserIdentifierResolver.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/NameIdUserIdentifierResolver.java index c21e1493a..ec7559a8c 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/NameIdUserIdentifierResolver.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/NameIdUserIdentifierResolver.java @@ -1,26 +1,39 @@ package it.infn.mw.iam.authn.saml.util; -import java.util.Optional; - import org.opensaml.saml2.core.NameID; import org.springframework.security.saml.SAMLCredential; import com.google.common.base.Verify; -public class NameIdUserIdentifierResolver implements SamlUserIdentifierResolver { +import it.infn.mw.iam.persistence.model.IamSamlId; - @Override - public Optional getUserIdentifier(SAMLCredential samlCredential) { +public class NameIdUserIdentifierResolver extends AbstractSamlUserIdentifierResolver{ - Verify.verifyNotNull(samlCredential); + public static final String NAMEID_RESOLVER = "nameID"; + + public NameIdUserIdentifierResolver() { + super(NAMEID_RESOLVER); + } + @Override + public SamlUserIdentifierResolutionResult resolveSamlUserIdentifier( + SAMLCredential samlCredential) { + + Verify.verifyNotNull(samlCredential); + if (samlCredential.getNameID() != null) { NameID nameId = samlCredential.getNameID(); - return Optional.of(nameId.getValue()); + + IamSamlId samlId = new IamSamlId(); + samlId.setAttributeId(nameId.getFormat()); + samlId.setUserId(nameId.getValue()); + samlId.setIdpId(samlCredential.getRemoteEntityID()); + + return SamlUserIdentifierResolutionResult.resolutionSuccess(samlId); } - return Optional.empty(); + return SamlUserIdentifierResolutionResult.resolutionFailure("NameID resolution failure"); } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/NamedSamlUserIdentifierResolver.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/NamedSamlUserIdentifierResolver.java new file mode 100644 index 000000000..ffe2dfce9 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/NamedSamlUserIdentifierResolver.java @@ -0,0 +1,7 @@ +package it.infn.mw.iam.authn.saml.util; + +public interface NamedSamlUserIdentifierResolver extends SamlUserIdentifierResolver { + + public String getName(); + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/Saml2Attribute.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/Saml2Attribute.java new file mode 100644 index 000000000..9233d9d43 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/Saml2Attribute.java @@ -0,0 +1,32 @@ +package it.infn.mw.iam.authn.saml.util; + +public enum Saml2Attribute { + + EPUID("eduPersonUniqueId", "urn:oid:1.3.6.1.4.1.5923.1.1.1.13"), + EPTID("eduPersonTargetedId", "urn:oid:1.3.6.1.4.1.5923.1.1.1.10"), + EPPN("eduPersonPrincipalName", "urn:oid:1.3.6.1.4.1.5923.1.1.1.6"), + EPORCID("eduPersonOrcid", "urn:oid:1.3.6.1.4.1.5923.1.1.1.16"), + MAIL("mail", "urn:oid:0.9.2342.19200300.100.1.3"), + GIVEN_NAME("givenName", "urn:oid:2.5.4.42"), + SN("sn", "urn:oid:2.5.4.4"), + CN("cn", "urn:oid:2.5.4.3"), + EMPLOYEE_NUMBER("employeeNumber", "urn:oid:2.16.840.1.113730.3.1.3"), + SPID_CODE("spidCode", "spidCode"); + + private String alias; + private String attributeName; + + private Saml2Attribute(String alias, String attributeName) { + this.alias = alias; + this.attributeName = attributeName; + } + + public String getAlias() { + return alias; + } + + public String getAttributeName() { + return attributeName; + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/SamlIdResolvers.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/SamlIdResolvers.java index 93cb0f589..335b471c4 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/SamlIdResolvers.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/SamlIdResolvers.java @@ -1,26 +1,33 @@ package it.infn.mw.iam.authn.saml.util; +import com.google.common.collect.ImmutableMap; + public class SamlIdResolvers { - public static final String EPUID_NAME = "urn:oid:1.3.6.1.4.1.5923.1.1.1.13"; - public static final String EPPN_NAME = "urn:oid:1.3.6.1.4.1.5923.1.1.1.6"; - public static final String MAIL_NAME = "urn:oid:0.9.2342.19200300.100.1.3"; + public static final String NAME_ID_NAME = "nameID"; + + private ImmutableMap registeredResolvers; - private SamlIdResolvers() {} + public SamlIdResolvers() { + ImmutableMap.Builder builder = + ImmutableMap.builder(); - public static SamlUserIdentifierResolver epuid() { - return new AttributeUserIdentifierResolver(EPUID_NAME); - } + for (Saml2Attribute a : Saml2Attribute.values()) { + builder.put(a.getAlias(), new AttributeUserIdentifierResolver(a)); + } - public static SamlUserIdentifierResolver eppn() { - return new AttributeUserIdentifierResolver(EPPN_NAME); - } + builder.put(NAME_ID_NAME, new NameIdUserIdentifierResolver()); - public static SamlUserIdentifierResolver mail() { - return new AttributeUserIdentifierResolver(MAIL_NAME); + registeredResolvers = builder.build(); } - public static SamlUserIdentifierResolver nameid() { - return new NameIdUserIdentifierResolver(); + public SamlUserIdentifierResolver byAttribute(Saml2Attribute attribute) { + return registeredResolvers.get(attribute.getAlias()); } + + public SamlUserIdentifierResolver byName(String name) { + return registeredResolvers.get(name); + } + + } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/SamlUserIdentifierResolutionResult.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/SamlUserIdentifierResolutionResult.java new file mode 100644 index 000000000..ec9208a42 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/SamlUserIdentifierResolutionResult.java @@ -0,0 +1,51 @@ +package it.infn.mw.iam.authn.saml.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import it.infn.mw.iam.persistence.model.IamSamlId; + +public class SamlUserIdentifierResolutionResult { + + + final Optional resolvedId; + final Optional> errorMessages; + + private SamlUserIdentifierResolutionResult(IamSamlId resolvedId){ + this.resolvedId = Optional.of(resolvedId); + this.errorMessages = Optional.empty(); + } + + private SamlUserIdentifierResolutionResult(String errorMessage){ + this.resolvedId = Optional.empty(); + List errors = new ArrayList<>(); + errors.add(errorMessage); + this.errorMessages = Optional.of(errors); + } + + private SamlUserIdentifierResolutionResult(List errorMessages){ + this.resolvedId = Optional.empty(); + this.errorMessages = Optional.of(errorMessages); + } + + public Optional getResolvedId() { + return resolvedId; + } + + public Optional> getErrorMessages() { + return errorMessages; + } + + public static SamlUserIdentifierResolutionResult resolutionSuccess(IamSamlId resolvedId){ + return new SamlUserIdentifierResolutionResult(resolvedId); + } + + public static SamlUserIdentifierResolutionResult resolutionFailure(String errorMessage){ + return new SamlUserIdentifierResolutionResult(errorMessage); + } + + public static SamlUserIdentifierResolutionResult resolutionFailure(List errorMessages) { + return new SamlUserIdentifierResolutionResult(errorMessages); + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/SamlUserIdentifierResolver.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/SamlUserIdentifierResolver.java index 2b62ee533..025869123 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/SamlUserIdentifierResolver.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/util/SamlUserIdentifierResolver.java @@ -1,11 +1,7 @@ package it.infn.mw.iam.authn.saml.util; -import java.util.Optional; - import org.springframework.security.saml.SAMLCredential; - +@FunctionalInterface public interface SamlUserIdentifierResolver { - - public Optional getUserIdentifier(SAMLCredential samlCredential); - + public SamlUserIdentifierResolutionResult resolveSamlUserIdentifier(SAMLCredential samlCredential); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/web/SamlRefreshMetadataController.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/web/SamlRefreshMetadataController.java new file mode 100644 index 000000000..c2911c5ca --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/web/SamlRefreshMetadataController.java @@ -0,0 +1,31 @@ +package it.infn.mw.iam.authn.saml.web; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.saml.metadata.MetadataManager; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import it.infn.mw.iam.authn.saml.MetadataLookupService; + +@RestController +@Profile("saml") +public class SamlRefreshMetadataController { + + @Autowired + MetadataManager metadataManager; + + @Autowired + MetadataLookupService metadataLookupService; + + @PreAuthorize("hasRole('ADMIN')") + @RequestMapping(value="/saml/refresh-metadata") + public String refreshMetadata() { + metadataManager.setRefreshRequired(true); + metadataManager.refreshMetadata(); + metadataLookupService.refreshMetadata(); + return "ok"; + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/web/SamlSsoController.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/web/SamlSsoController.java index 2d59c70c6..4122cbbf5 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/web/SamlSsoController.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/saml/web/SamlSsoController.java @@ -25,7 +25,7 @@ public class SamlSsoController { @Autowired MetadataLookupService lookupService; - + @RequestMapping(value = "/idps", method = RequestMethod.GET) public @ResponseBody List idps( @RequestParam(value = "q", required = false) String text) { @@ -43,4 +43,5 @@ public String selectIdp(@RequestParam("entityID") String entityId, @RequestParam("returnIDParam") String returnIDParam) { return "iam/samlDiscovery"; } + } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/util/AuthenticationUtils.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/util/AuthenticationUtils.java index 7965c56c3..f4327fba2 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/authn/util/AuthenticationUtils.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/util/AuthenticationUtils.java @@ -1,12 +1,19 @@ package it.infn.mw.iam.authn.util; +import java.util.List; +import java.util.stream.Collectors; + import org.mitre.oauth2.model.SavedUserAuthentication; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; import com.google.common.collect.ImmutableSet; import it.infn.mw.iam.authn.oidc.OidcExternalAuthenticationToken; import it.infn.mw.iam.authn.saml.SamlExternalAuthenticationToken; +import it.infn.mw.iam.persistence.model.IamAccount; public class AuthenticationUtils { @@ -14,6 +21,8 @@ public class AuthenticationUtils { ImmutableSet.of(SamlExternalAuthenticationToken.class.getName(), OidcExternalAuthenticationToken.class.getName()); + private AuthenticationUtils() {} + public static boolean isSupportedExternalAuthenticationToken(Authentication authn) { if (authn instanceof SavedUserAuthentication) { @@ -27,6 +36,17 @@ public static boolean isSupportedExternalAuthenticationToken(Authentication auth return false; } - private AuthenticationUtils() {} + public static List convertIamAccountAuthorities(IamAccount account) { + return account.getAuthorities() + .stream() + .map(a -> new SimpleGrantedAuthority(a.getAuthority())) + .collect(Collectors.toList()); + } + + public static User userFromIamAccount(IamAccount account){ + return new User(account.getUsername(), account.getPassword(), convertIamAccountAuthorities(account)); + } + + } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/CertificateParsingError.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/CertificateParsingError.java new file mode 100644 index 000000000..70ed02516 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/CertificateParsingError.java @@ -0,0 +1,17 @@ +package it.infn.mw.iam.authn.x509; + +public class CertificateParsingError extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public CertificateParsingError(String message) { + super(message); + } + + public CertificateParsingError(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/DefaultX509AuthenticationCredentialExtractor.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/DefaultX509AuthenticationCredentialExtractor.java new file mode 100644 index 000000000..69d9a3854 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/DefaultX509AuthenticationCredentialExtractor.java @@ -0,0 +1,127 @@ +package it.infn.mw.iam.authn.x509; + +import static it.infn.mw.iam.authn.x509.DefaultX509AuthenticationCredentialExtractor.Headers.CLIENT_CERT; +import static it.infn.mw.iam.authn.x509.DefaultX509AuthenticationCredentialExtractor.Headers.ISSUER; +import static it.infn.mw.iam.authn.x509.DefaultX509AuthenticationCredentialExtractor.Headers.SUBJECT; +import static it.infn.mw.iam.authn.x509.DefaultX509AuthenticationCredentialExtractor.Headers.VERIFY; + +import java.util.EnumSet; +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.google.common.base.Strings; + +@Component +public class DefaultX509AuthenticationCredentialExtractor + implements X509AuthenticationCredentialExtractor { + + public enum Headers { + CLIENT_CERT("X-SSL-Client-Cert"), + SUBJECT("X-SSL-Client-S-Dn"), + ISSUER("X-SSL-Client-I-Dn"), + SERIAL("X-SSL-Client-Serial"), + VERIFY("X-SSL-Client-Verify"), + V_START("X-SSL-Client-V-Start"), + V_END("X-SSL-Client-V-End"), + PROTOCOL("X-SSL-Protocol"), + SERVER_NAME("X-SSL-Server-Name"); + + private final String header; + + private Headers(String header) { + this.header = header; + } + + public String getHeader() { + return header; + } + } + + public static final Logger LOG = + LoggerFactory.getLogger(DefaultX509AuthenticationCredentialExtractor.class); + + private final X509CertificateChainParser certChainParser; + + protected static final EnumSet HEADERS_REQUIRED = + EnumSet.complementOf(EnumSet.of(Headers.SERVER_NAME)); + + @Autowired + public DefaultX509AuthenticationCredentialExtractor(X509CertificateChainParser chainParser) { + this.certChainParser = chainParser; + } + + private String getHeader(HttpServletRequest request, Headers header){ + return request.getHeader(header.header); + } + + private void headerNamesSanityChecks(HttpServletRequest request) { + for (Headers e : HEADERS_REQUIRED) { + if (Strings.isNullOrEmpty(request.getHeader(e.header))) { + throw new IllegalArgumentException("Required header not found: " + e.header); + } + } + } + + private X509CertificateVerificationResult parseVerifyHeader(HttpServletRequest request) { + String verifyHeaderContent = request.getHeader(VERIFY.header); + + if ("SUCCESS".equals(verifyHeaderContent)) { + return X509CertificateVerificationResult.success(); + } + + // NGINX returns a client certificate validation failure in the following form: + // FAILED:reason + if (verifyHeaderContent.startsWith("FAILED:")) { + String reason = verifyHeaderContent.substring(7); // skip the "FAILED:" preamble + return X509CertificateVerificationResult.failed(reason); + } + + final String errorMsg = + String.format("Could not parse X.509 certificate verification header: %s : %s", + VERIFY.header, verifyHeaderContent); + + LOG.error(errorMsg); + throw new IllegalArgumentException(errorMsg); + + } + + @Override + public Optional extractX509Credential( + HttpServletRequest request) { + + String clientCertHeaderContent = getHeader(request, CLIENT_CERT); + + if (Strings.isNullOrEmpty(clientCertHeaderContent)) { + LOG.debug("{} null or empty", CLIENT_CERT.header); + return Optional.empty(); + } + + headerNamesSanityChecks(request); + + String pemCertificateString = clientCertHeaderContent.replace('\t', '\n'); + + X509CertificateChainParsingResult chain = + certChainParser.parseChainFromString(pemCertificateString); + + IamX509AuthenticationCredential.Builder credBuilder = + new IamX509AuthenticationCredential.Builder(); + + // FIXME: populate all the fields we get from NGINX + credBuilder.certificateChain(chain.getChain()) + .certificateChainPemString(chain.getPemString()) + .subject(getHeader(request, SUBJECT)) + .issuer(getHeader(request, ISSUER)) + .verificationResult(parseVerifyHeader(request)); + + final IamX509AuthenticationCredential cred = credBuilder.build(); + LOG.debug("Extracted X.509 credential: {}", cred); + return Optional.of(cred); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/IamX509AuthenticationCredential.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/IamX509AuthenticationCredential.java new file mode 100644 index 000000000..116f9d4cf --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/IamX509AuthenticationCredential.java @@ -0,0 +1,111 @@ +package it.infn.mw.iam.authn.x509; + +import java.io.Serializable; +import java.security.cert.X509Certificate; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import it.infn.mw.iam.persistence.model.IamX509Certificate; + +public class IamX509AuthenticationCredential implements Serializable{ + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final String subject; + private final String issuer; + + @JsonIgnore + private final X509Certificate[] certificateChain; + + private final String certificateChainPemString; + + @JsonIgnore + private final X509CertificateVerificationResult verificationResult; + + private IamX509AuthenticationCredential(Builder builder) { + this.subject = builder.subject; + this.issuer = builder.issuer; + this.certificateChain = builder.certificateChain; + this.verificationResult = builder.verificationResult; + this.certificateChainPemString = builder.certificateChainPemString; + } + + public static class Builder { + private String subject; + private String issuer; + private X509Certificate[] certificateChain; + private String certificateChainPemString; + private X509CertificateVerificationResult verificationResult; + + public Builder subject(String subject){ + this.subject = subject; + return this; + } + + public Builder issuer(String issuer){ + this.issuer = issuer; + return this; + } + + public Builder certificateChain(X509Certificate[] chain){ + this.certificateChain = chain; + return this; + } + + public Builder verificationResult(X509CertificateVerificationResult s){ + this.verificationResult = s; + return this; + } + + public Builder certificateChainPemString(String ccps){ + this.certificateChainPemString =ccps; + return this; + } + + public IamX509AuthenticationCredential build(){ + return new IamX509AuthenticationCredential(this); + } + } + + public IamX509Certificate asIamX509Certificate(){ + IamX509Certificate cert = new IamX509Certificate(); + cert.setSubjectDn(getSubject()); + cert.setIssuerDn(getIssuer()); + cert.setCertificate(getCertificateChainPemString()); + return cert; + } + + public String getSubject() { + return subject; + } + + public String getIssuer() { + return issuer; + } + + public X509Certificate[] getCertificateChain() { + return certificateChain; + } + + public String getCertificateChainPemString() { + return certificateChainPemString; + } + public X509CertificateVerificationResult getVerificationResult() { + return verificationResult; + } + + public boolean failedVerification(){ + return verificationResult.failedVerification(); + } + + public String verificationError(){ + return verificationResult.error().orElse("X.509 credential is valid"); + } + + public static Builder builder(){ + return new Builder(); + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/IamX509AuthenticationProvider.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/IamX509AuthenticationProvider.java new file mode 100644 index 000000000..8954aad46 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/IamX509AuthenticationProvider.java @@ -0,0 +1,9 @@ +package it.infn.mw.iam.authn.x509; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider; + +public class IamX509AuthenticationProvider extends PreAuthenticatedAuthenticationProvider + implements AuthenticationManager { + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/IamX509AuthenticationUserDetailService.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/IamX509AuthenticationUserDetailService.java new file mode 100644 index 000000000..53ea25359 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/IamX509AuthenticationUserDetailService.java @@ -0,0 +1,58 @@ +package it.infn.mw.iam.authn.x509; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.stereotype.Service; + +import it.infn.mw.iam.authn.InactiveAccountAuthenticationHander; +import it.infn.mw.iam.authn.util.AuthenticationUtils; +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.repository.IamAccountRepository; + +@Service +public class IamX509AuthenticationUserDetailService + implements AuthenticationUserDetailsService { + + public static final Logger LOG = + LoggerFactory.getLogger(IamX509AuthenticationUserDetailService.class); + + IamAccountRepository accountRepository; + InactiveAccountAuthenticationHander inactiveAccountHandler; + + @Autowired + public IamX509AuthenticationUserDetailService(IamAccountRepository accountRepository, + InactiveAccountAuthenticationHander handler) { + this.accountRepository = accountRepository; + this.inactiveAccountHandler = handler; + } + + protected UserDetails buildUserFromIamAccount(IamAccount account) { + return AuthenticationUtils.userFromIamAccount(account); + } + + @Override + public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) + throws UsernameNotFoundException { + + String principal = (String) token.getPrincipal(); + + LOG.debug("Loading IAM account for X.509 principal '{}'", principal); + + IamAccount account = accountRepository.findByCertificateSubject(principal).orElseThrow(() -> { + final String msg = String.format("No IAM account found for X.509 principal '%s'", principal); + LOG.debug(msg); + return new UsernameNotFoundException(msg); + }); + + LOG.debug("Found IAM account {} linked to principal '{}'", account, principal); + + return buildUserFromIamAccount(account); + + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/IamX509PreauthenticationProcessingFilter.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/IamX509PreauthenticationProcessingFilter.java new file mode 100644 index 000000000..4ad4e7dd4 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/IamX509PreauthenticationProcessingFilter.java @@ -0,0 +1,132 @@ +package it.infn.mw.iam.authn.x509; + +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; + +public class IamX509PreauthenticationProcessingFilter + extends AbstractPreAuthenticatedProcessingFilter { + + public static final Logger LOG = + LoggerFactory.getLogger(IamX509PreauthenticationProcessingFilter.class); + + public static final String X509_CREDENTIAL_SESSION_KEY = "IAM_X509_CRED"; + + private final X509AuthenticationCredentialExtractor credentialExtractor; + + private AuthenticationSuccessHandler successHandler; + private AuthenticationFailureHandler failureHandler; + + public IamX509PreauthenticationProcessingFilter(X509AuthenticationCredentialExtractor extractor, + AuthenticationManager authenticationManager) { + this.credentialExtractor = extractor; + setCheckForPrincipalChanges(false); + setAuthenticationManager(authenticationManager); + } + + protected void storeCredentialInSession(HttpServletRequest request, + IamX509AuthenticationCredential cred) { + + HttpSession session = request.getSession(false); + + if (session != null && !cred.failedVerification()) { + LOG.debug("Storing X.509 {} credential in session ", cred); + session.setAttribute(X509_CREDENTIAL_SESSION_KEY, cred); + } + + } + + + protected Optional extractCredential( + HttpServletRequest request) { + Optional credential = + credentialExtractor.extractX509Credential(request); + + if (!credential.isPresent()) { + LOG.debug("No X.509 client credential found in request"); + } + + if (credential.isPresent() && credential.get().failedVerification()) { + LOG.warn("X.509 client credential failed verification: {}", + credential.get().verificationError()); + return Optional.empty(); + } + + credential.ifPresent(c -> storeCredentialInSession(request, c)); + + return credential; + } + + + @Override + protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) { + + Optional credential = extractCredential(request); + + if (!credential.isPresent()) { + return null; + } + + final String subject = credential.get().getSubject(); + + LOG.debug("Found valid X.509 credential in request with principal subject '{}'", subject); + + return subject; + } + + @Override + protected Object getPreAuthenticatedCredentials(HttpServletRequest request) { + + return extractCredential(request).orElse(null); + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) { + + super.successfulAuthentication(request, response, authentication); + + if (successHandler != null) { + try { + successHandler.onAuthenticationSuccess(request, response, authentication); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + } + + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, + HttpServletResponse response, AuthenticationException failed) { + + super.unsuccessfulAuthentication(request, response, failed); + + if (failureHandler != null) { + try { + failureHandler.onAuthenticationFailure(request, response, failed); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + } + + public void setSuccessHandler(AuthenticationSuccessHandler successHandler) { + this.successHandler = successHandler; + } + + public void setFailureHandler(AuthenticationFailureHandler failureHandler) { + this.failureHandler = failureHandler; + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/MockX509AuthenticationCredentialExtractor.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/MockX509AuthenticationCredentialExtractor.java new file mode 100644 index 000000000..2d5e76521 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/MockX509AuthenticationCredentialExtractor.java @@ -0,0 +1,65 @@ +package it.infn.mw.iam.authn.x509; + +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +@Component +@Primary +@ConditionalOnProperty("mockX509Authentication") +public class MockX509AuthenticationCredentialExtractor + implements X509AuthenticationCredentialExtractor { + + public static final String TEST_0_SUBJECT = "CN=test0,O=IGI,C=IT"; + public static final String TEST_0_ISSUER = "CN=Test CA,O=IGI,C=IT"; + private static final String TEST_0_CERT = "-----BEGIN CERTIFICATE-----\n" + + "MIIDnjCCAoagAwIBAgIBCTANBgkqhkiG9w0BAQUFADAtMQswCQYDVQQGEwJJVDEM\n" + + "MAoGA1UECgwDSUdJMRAwDgYDVQQDDAdUZXN0IENBMB4XDTEyMDkyNjE1MzkzNFoX\n" + + "DTIyMDkyNDE1MzkzNFowKzELMAkGA1UEBhMCSVQxDDAKBgNVBAoTA0lHSTEOMAwG\n" + + "A1UEAxMFdGVzdDAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKxtrw\n" + + "hoZ27SxxISjlRqWmBWB6U+N/xW2kS1uUfrQRav6auVtmtEW45J44VTi3WW6Y113R\n" + + "BwmS6oW+3lzyBBZVPqnhV9/VkTxLp83gGVVvHATgGgkjeTxIsOE+TkPKAoZJ/QFc\n" + + "CfPh3WdZ3ANI14WYkAM9VXsSbh2okCsWGa4o6pzt3Pt1zKkyO4PW0cBkletDImJK\n" + + "2vufuDVNm7Iz/y3/8pY8p3MoiwbF/PdSba7XQAxBWUJMoaleh8xy8HSROn7tF2al\n" + + "xoDLH4QWhp6UDn2rvOWseBqUMPXFjsUi1/rkw1oHAjMroTk5lL15GI0LGd5dTVop\n" + + "kKXFbTTYxSkPz1MLAgMBAAGjgcowgccwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU\n" + + "fLdB5+jO9LyWN2/VCNYgMa0jvHEwDgYDVR0PAQH/BAQDAgXgMD4GA1UdJQQ3MDUG\n" + + "CCsGAQUFBwMBBggrBgEFBQcDAgYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBBggrBgEF\n" + + "BQcDBDAfBgNVHSMEGDAWgBSRdzZ7LrRp8yfqt/YIi0ojohFJxjAnBgNVHREEIDAe\n" + + "gRxhbmRyZWEuY2VjY2FudGlAY25hZi5pbmZuLml0MA0GCSqGSIb3DQEBBQUAA4IB\n" + + "AQANYtWXetheSeVpCfnId9TkKyKTAp8RahNZl4XFrWWn2S9We7ACK/G7u1DebJYx\n" + + "d8POo8ClscoXyTO2BzHHZLxauEKIzUv7g2GehI+SckfZdjFyRXjD0+wMGwzX7MDu\n" + + "SL3CG2aWsYpkBnj6BMlr0P3kZEMqV5t2+2Tj0+aXppBPVwzJwRhnrSJiO5WIZAZf\n" + + "49YhMn61sQIrepvhrKEUR4XVorH2Bj8ek1/iLlgcmFMBOds+PrehSRR8Gn0IjlEg\n" + + "C68EY6KPE+FKySuS7Ur7lTAjNdddfdAgKV6hJyST6/dx8ymIkb8nxCPnxCcT2I2N\n" + + "vDxcPMc/wmnMa+smNal0sJ6m\n" + "-----END CERTIFICATE-----"; + + private final IamX509AuthenticationCredential test0Cred; + + @Autowired + public MockX509AuthenticationCredentialExtractor(X509CertificateChainParser parser) { + + X509CertificateChainParsingResult result = parser.parseChainFromString(TEST_0_CERT); + test0Cred = IamX509AuthenticationCredential.builder() + .certificateChain(result.getChain()) + .certificateChainPemString(result.getPemString()) + .subject(TEST_0_SUBJECT) + .issuer(TEST_0_ISSUER) + .verificationResult(X509CertificateVerificationResult.success()) + .build(); + } + + + @Override + public Optional extractX509Credential( + HttpServletRequest request) { + + return Optional.of(test0Cred); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/PEMX509CertificateChainParser.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/PEMX509CertificateChainParser.java new file mode 100644 index 000000000..014da6301 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/PEMX509CertificateChainParser.java @@ -0,0 +1,48 @@ +package it.infn.mw.iam.authn.x509; + +import static eu.emi.security.authn.x509.impl.CertificateUtils.configureSecProvider; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import eu.emi.security.authn.x509.impl.CertificateUtils; +import eu.emi.security.authn.x509.impl.CertificateUtils.Encoding; + +@Component +public class PEMX509CertificateChainParser implements X509CertificateChainParser { + + public static final Logger LOG = LoggerFactory.getLogger(PEMX509CertificateChainParser.class); + + public PEMX509CertificateChainParser() { + configureSecProvider(); + } + + @Override + public X509CertificateChainParsingResult parseChainFromString(String pemString) { + + InputStream stream = new ByteArrayInputStream(pemString + .getBytes(StandardCharsets.US_ASCII)); + + try { + + X509Certificate[] chain = CertificateUtils.loadCertificateChain(stream, Encoding.PEM); + return X509CertificateChainParsingResult.from(pemString, chain); + + } catch (IOException e) { + final String errorMessage = String.format("Error parsing certificate chain: %s", + e.getMessage()); + + LOG.error(errorMessage, e); + + throw new CertificateParsingError(errorMessage, e); + } + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/X509AuthenticationCredentialExtractor.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/X509AuthenticationCredentialExtractor.java new file mode 100644 index 000000000..022a3fe49 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/X509AuthenticationCredentialExtractor.java @@ -0,0 +1,10 @@ +package it.infn.mw.iam.authn.x509; + +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; +@FunctionalInterface +public interface X509AuthenticationCredentialExtractor { + + Optional extractX509Credential(HttpServletRequest request); +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/X509CertificateChainParser.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/X509CertificateChainParser.java new file mode 100644 index 000000000..383c5e3fe --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/X509CertificateChainParser.java @@ -0,0 +1,7 @@ +package it.infn.mw.iam.authn.x509; +@FunctionalInterface +public interface X509CertificateChainParser { + + X509CertificateChainParsingResult parseChainFromString(String pemCertificateChain); + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/X509CertificateChainParsingResult.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/X509CertificateChainParsingResult.java new file mode 100644 index 000000000..bf5329a68 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/X509CertificateChainParsingResult.java @@ -0,0 +1,27 @@ +package it.infn.mw.iam.authn.x509; + +import java.security.cert.X509Certificate; + +public class X509CertificateChainParsingResult { + + private final String pemString; + private final X509Certificate[] chain; + + private X509CertificateChainParsingResult(String pemString, X509Certificate[] chain) { + this.pemString = pemString; + this.chain = chain; + } + + public String getPemString() { + return pemString; + } + + public X509Certificate[] getChain() { + return chain; + } + + public static X509CertificateChainParsingResult from(String pemString, X509Certificate[] chain){ + return new X509CertificateChainParsingResult(pemString, chain); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/X509CertificateVerificationResult.java b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/X509CertificateVerificationResult.java new file mode 100644 index 000000000..53966fcf0 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/authn/x509/X509CertificateVerificationResult.java @@ -0,0 +1,51 @@ +package it.infn.mw.iam.authn.x509; + +import java.io.Serializable; +import java.util.Optional; + +public class X509CertificateVerificationResult implements Serializable{ + + /** + * + */ + private static final long serialVersionUID = 1L; + + public enum Status { + SUCCESS, + FAILED + } + + final Status verificationStatus; + final transient Optional verificationError; + + private X509CertificateVerificationResult(Status s, String verificationError) { + this.verificationStatus = s; + this.verificationError = Optional.ofNullable(verificationError); + } + + public Status status() { + return verificationStatus; + } + + public Optional error() { + return verificationError; + } + + public static X509CertificateVerificationResult success(){ + return new X509CertificateVerificationResult(Status.SUCCESS, null); + } + + public static X509CertificateVerificationResult failed(String reason){ + return new X509CertificateVerificationResult(Status.FAILED, reason); + } + + public boolean failedVerification(){ + return verificationStatus == Status.FAILED; + } + + @Override + public String toString() { + return "X509CertificateVerificationResult [verificationStatus=" + verificationStatus + + ", verificationError=" + verificationError + "]"; + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/AssertionConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/AssertionConfig.java index dca5d34e9..0f7fadd03 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/AssertionConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/AssertionConfig.java @@ -30,7 +30,6 @@ public AssertionOAuth2RequestFactory jwtAssertionTokenFactory() { @Bean @Qualifier("clientAssertionValidator") public AssertionValidator clientAssertionValidator() { - // TODO: verify whitelist Map whitelist = new LinkedHashMap<>(); whitelist.put("http://artemesia.local", "http://localhost:8080/jwk"); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/AuthorizationServerConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/AuthorizationServerConfig.java index c7a65f2ef..b646255ab 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/AuthorizationServerConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/AuthorizationServerConfig.java @@ -53,6 +53,7 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap private OAuth2TokenEntityService tokenServices; @Autowired + @Qualifier("iamClientDetailsEntityService") private ClientDetailsEntityService clientDetailsService; @Autowired @@ -72,7 +73,7 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap @Autowired private SystemScopeService systemScopeService; - + @Bean WebResponseExceptionTranslator webResponseExceptionTranslator() { @@ -81,10 +82,10 @@ WebResponseExceptionTranslator webResponseExceptionTranslator() { } @Bean(name = "iamAuthenticationEventPublisher") - AuthenticationEventPublisher iamAuthenticationEventPublisher(){ + AuthenticationEventPublisher iamAuthenticationEventPublisher() { return new IamAuthenticationEventPublisher(); } - + @Bean(name = "authenticationManager") AuthenticationManager authenticationManager() { diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/JpaConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/JpaConfig.java index 6d98ac69a..5e2107919 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/JpaConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/JpaConfig.java @@ -36,16 +36,16 @@ protected AbstractJpaVendorAdapter createJpaVendorAdapter() { @Override protected Map getVendorProperties() { - Map map = new HashMap(); + Map map = new HashMap<>(); map.put("eclipselink.weaving", "false"); map.put("eclipselink.logging.level", "INFO"); map.put("eclipselink.logging.level.sql", "OFF"); map.put("eclipselink.cache.shared.default", "false"); - // map.put("eclipselink.ddl-generation.output-mode", "sql-script"); - // map.put("eclipselink.ddl-generation", "create-tables"); - // map.put("eclipselink.create-ddl-jdbc-file-name", "ddl.sql"); +// map.put("eclipselink.ddl-generation.output-mode", "sql-script"); +// map.put("eclipselink.ddl-generation", "create-tables"); +// map.put("eclipselink.create-ddl-jdbc-file-name", "ddl.sql"); return map; @@ -56,14 +56,12 @@ public LocalContainerEntityManagerFactoryBean entityManagerFactory( final EntityManagerFactoryBuilder factoryBuilder) { - LocalContainerEntityManagerFactoryBean emf = factoryBuilder.dataSource(dataSource) + return factoryBuilder.dataSource(dataSource) .packages("org.mitre", "it.infn.mw.iam.persistence") .persistenceUnit("defaultPersistenceUnit") .properties(getVendorProperties()) .build(); - return emf; - } @Bean(name = {"defaultTransactionManager", "transactionManager"}) diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/MitreConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/MitreConfig.java index 2e90de12e..7c7974a57 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/MitreConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/MitreConfig.java @@ -38,7 +38,13 @@ public ConfigurationPropertiesBean config() { } config.setIssuer(issuer); - config.setRegTokenLifeTime(tokenLifeTime); + + if (tokenLifeTime <= 0L){ + config.setRegTokenLifeTime(null); + } else { + config.setRegTokenLifeTime(tokenLifeTime); + } + config.setForceHttps(false); config.setLocale(Locale.ENGLISH); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/MitreServicesConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/MitreServicesConfig.java index 76e9111a4..1a492191d 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/MitreServicesConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/MitreServicesConfig.java @@ -42,9 +42,11 @@ import org.mitre.openid.connect.web.ServerConfigInterceptor; import org.mitre.openid.connect.web.UserInfoInterceptor; import org.mitre.uma.service.ResourceSetService; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.embedded.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.oauth2.provider.OAuth2RequestFactory; import org.springframework.security.oauth2.provider.OAuth2RequestValidator; @@ -80,12 +82,12 @@ UserApprovalHandler tofuApprovalHandler() { @Bean OAuth2RequestFactory requestFactory() { - - return new IamOAuth2RequestFactory(clientDetailsService()); + return new IamOAuth2RequestFactory(clientDetailsEntityService()); } @Bean - ClientDetailsEntityService clientDetailsService() { + @Qualifier("iamClientDetailsEntityService") + ClientDetailsEntityService clientDetailsEntityService() { return new DefaultOAuth2ClientDetailsEntityService(); } @@ -143,6 +145,7 @@ public FilterRegistrationBean disabledCorsFilterRegistration(CorsFilter c) { return b; } + @Primary @Bean public CorsFilter corsFilter() { diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/SecurityConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/SecurityConfig.java index 7fe6031b4..499fc5bbd 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/SecurityConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/SecurityConfig.java @@ -1,5 +1,19 @@ package it.infn.mw.iam.config; +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 it.infn.mw.iam.authn.RootIsDashboardSuccessHandler; +import it.infn.mw.iam.authn.TimestamperSuccessHandler; +import it.infn.mw.iam.authn.oidc.OidcAccessDeniedHandler; +import it.infn.mw.iam.authn.oidc.OidcAuthenticationProvider; +import it.infn.mw.iam.authn.oidc.OidcClientFilter; +import it.infn.mw.iam.authn.x509.IamX509AuthenticationProvider; +import it.infn.mw.iam.authn.x509.IamX509AuthenticationUserDetailService; +import it.infn.mw.iam.authn.x509.IamX509PreauthenticationProcessingFilter; +import it.infn.mw.iam.authn.x509.X509AuthenticationCredentialExtractor; +import it.infn.mw.iam.persistence.repository.IamAccountRepository; + import org.mitre.oauth2.service.OAuth2TokenEntityService; import org.mitre.oauth2.web.CorsFilter; import org.springframework.beans.factory.annotation.Autowired; @@ -37,18 +51,10 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.web.filter.GenericFilterBean; -import it.infn.mw.iam.authn.RootIsDashboardSuccessHandler; -import it.infn.mw.iam.authn.TimestamperSuccessHandler; -import it.infn.mw.iam.authn.oidc.OidcAccessDeniedHandler; -import it.infn.mw.iam.authn.oidc.OidcAuthenticationProvider; -import it.infn.mw.iam.authn.oidc.OidcClientFilter; - @Configuration @EnableWebSecurity public class SecurityConfig { - - @Configuration @Order(100) public static class UserLoginConfig extends WebSecurityConfigurerAdapter { @@ -67,9 +73,18 @@ public static class UserLoginConfig extends WebSecurityConfigurerAdapter { @Qualifier("iamUserDetailsService") private UserDetailsService iamUserDetailsService; + @Autowired + private X509AuthenticationCredentialExtractor x509CredentialExtractor; + + @Autowired + private IamX509AuthenticationUserDetailService x509UserDetailsService; + @Autowired private PasswordEncoder passwordEncoder; + @Autowired + private IamAccountRepository accountRepo; + @Autowired public void configureGlobal(final AuthenticationManagerBuilder auth) throws Exception { // @formatter:off @@ -85,6 +100,20 @@ public void configure(final WebSecurity web) throws Exception { web.expressionHandler(oAuth2WebSecurityExpressionHandler); } + + public IamX509AuthenticationProvider iamX509AuthenticationProvider() { + + IamX509AuthenticationProvider provider = new IamX509AuthenticationProvider(); + provider.setPreAuthenticatedUserDetailsService(x509UserDetailsService); + return provider; + } + + + public IamX509PreauthenticationProcessingFilter iamX509Filter() throws Exception { + return new IamX509PreauthenticationProcessingFilter(x509CredentialExtractor, + iamX509AuthenticationProvider()); + } + @Override protected void configure(final HttpSecurity http) throws Exception { @@ -118,7 +147,9 @@ protected void configure(final HttpSecurity http) throws Exception { .and().anonymous() .and() .csrf() - .requireCsrfProtectionMatcher(new AntPathRequestMatcher("/authorize")).disable(); + .requireCsrfProtectionMatcher(new AntPathRequestMatcher("/authorize")).disable() + .addFilter(iamX509Filter()); + // @formatter:on } @@ -131,7 +162,8 @@ public OAuth2WebSecurityExpressionHandler oAuth2WebSecurityExpressionHandler() { public AuthenticationSuccessHandler successHandler() { return new TimestamperSuccessHandler( - new RootIsDashboardSuccessHandler(iamBaseUrl, new HttpSessionRequestCache())); + new RootIsDashboardSuccessHandler(iamBaseUrl, new HttpSessionRequestCache()), + accountRepo); } } @@ -425,7 +457,9 @@ protected void configure(final HttpSecurity http) throws Exception { http.antMatcher("/revoke**").httpBasic().authenticationEntryPoint(authenticationEntryPoint) .and().addFilterBefore(corsFilter, SecurityContextPersistenceFilter.class) .addFilterBefore(clientCredentialsEndpointFilter(), BasicAuthenticationFilter.class) - .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and() + .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint) + .and() + .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // @formatter:on } @@ -649,7 +683,7 @@ protected void configure(final HttpSecurity http) throws Exception { .requestMatchers() .antMatchers("/metrics", "/configprops", "/env", "/mappings", "/flyway", "/autoconfig", "/beans", "/dump", "/trace", - "/info", "/health", "/health/mail") + "/info", "/health", "/health/mail", "/health/external") .and() .httpBasic() .authenticationEntryPoint(authenticationEntryPoint) @@ -662,13 +696,52 @@ protected void configure(final HttpSecurity http) throws Exception { .sessionCreationPolicy(SessionCreationPolicy.NEVER) .and() .authorizeRequests() - .antMatchers(HttpMethod.GET, "/info", "/health", "/health/mail").permitAll() + .antMatchers(HttpMethod.GET, "/info", "/health", "/health/mail", "/health/external").permitAll() .antMatchers("/metrics", "/configprops", "/env", "/mappings", "/flyway", "/autoconfig", "/beans", "/dump", "/trace").hasRole("ADMIN"); // @formatter:on } } + @Configuration + @Order(25) + public static class TokensApiEndpointConfig extends WebSecurityConfigurerAdapter { + + @Autowired + private OAuth2AuthenticationProcessingFilter resourceFilter; + + @Autowired + private OAuth2AuthenticationEntryPoint authenticationEntryPoint; + + @Autowired + private CorsFilter corsFilter; + + @Override + protected void configure(final HttpSecurity http) throws Exception { + + // @formatter:off + http + .requestMatchers() + .antMatchers(ACCESS_TOKENS_ENDPOINT + "/**", REFRESH_TOKENS_ENDPOINT + "/**") + .and() + .exceptionHandling() + .authenticationEntryPoint(authenticationEntryPoint) + .and() + .addFilterAfter(resourceFilter, SecurityContextPersistenceFilter.class) + .addFilterBefore(corsFilter, WebAsyncManagerIntegrationFilter.class) + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.NEVER) + .and() + .authorizeRequests() + .antMatchers(ACCESS_TOKENS_ENDPOINT + "/**", REFRESH_TOKENS_ENDPOINT + "/**") + .authenticated() + .and() + .csrf() + .disable(); + // @formatter:on + } + } + @Configuration @Order(Ordered.HIGHEST_PRECEDENCE) @Profile("dev") diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/TaskConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/TaskConfig.java index 0c56e28d3..8af5a1f4d 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/TaskConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/TaskConfig.java @@ -5,6 +5,8 @@ import org.mitre.oauth2.service.OAuth2TokenEntityService; import org.mitre.openid.connect.service.ApprovedSiteService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; @@ -14,17 +16,22 @@ import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import it.infn.mw.iam.core.user.IamAccountService; import it.infn.mw.iam.notification.NotificationService; @Configuration @EnableScheduling public class TaskConfig implements SchedulingConfigurer { + public static final Logger LOG = LoggerFactory.getLogger(TaskConfig.class); + public static final long ONE_SECOND_MSEC = 1000; public static final long TEN_SECONDS_MSEC = 10 * ONE_SECOND_MSEC; public static final long THIRTY_SECONDS_MSEC = 30 * ONE_SECOND_MSEC; public static final long ONE_MINUTE_MSEC = 60 * ONE_SECOND_MSEC; public static final long TEN_MINUTES_MSEC = 10 * ONE_MINUTE_MSEC; + public static final long ONE_HOUR_MSEC = 60 * ONE_MINUTE_MSEC; + public static final long ONE_DAY_MSEC = 24 * ONE_HOUR_MSEC; @Autowired OAuth2TokenEntityService tokenEntityService; @@ -36,6 +43,9 @@ public class TaskConfig implements SchedulingConfigurer { @Qualifier("defaultNotificationService") NotificationService notificationService; + @Autowired + IamAccountService accountService; + @Bean(destroyMethod = "shutdown") public ScheduledExecutorService taskScheduler() { return Executors.newSingleThreadScheduledExecutor(); @@ -63,10 +73,9 @@ public void sendNotifications() { public void clearExpiredNotifications() { notificationService.clearExpiredNotifications(); } - + @Override public void configureTasks(final ScheduledTaskRegistrar taskRegistrar) { - taskRegistrar.setScheduler(taskScheduler()); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/oidc/GoogleClient.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/oidc/GoogleClient.java index 86d811c12..88a110790 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/oidc/GoogleClient.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/oidc/GoogleClient.java @@ -60,6 +60,9 @@ public class GoogleClient { @Autowired private GoogleClientProperties googleClientProperties; + @Autowired + private IamAccountRepository accountRepo; + @Bean public FilterRegistrationBean disabledAutomaticOidcFilterRegistration(OidcClientFilter f) { @@ -111,7 +114,8 @@ public AuthenticationSuccessHandler successHandler() { RootIsDashboardSuccessHandler sa = new RootIsDashboardSuccessHandler(iamBaseUrl, new HttpSessionRequestCache()); - AuthenticationSuccessHandler successHandler = new TimestamperSuccessHandler(sa); + + AuthenticationSuccessHandler successHandler = new TimestamperSuccessHandler(sa, accountRepo); return new ExternalAuthenticationSuccessHandler(successHandler, "/"); @@ -151,7 +155,7 @@ public ServerConfigurationService dynamicServerConfiguration() { @Bean public ClientConfigurationService staticClientConfiguration() { - Map clients = new LinkedHashMap(); + Map clients = new LinkedHashMap<>(); clients.put(googleClientProperties.getIssuer(), googleClientProperties); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/saml/IamSamlProperties.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/saml/IamSamlProperties.java index 9a3639cea..78e420ec2 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/saml/IamSamlProperties.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/saml/IamSamlProperties.java @@ -1,22 +1,104 @@ package it.infn.mw.iam.config.saml; +import static java.lang.Boolean.FALSE; + +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import javax.validation.constraints.Min; + import org.springframework.boot.context.properties.ConfigurationProperties; +import com.google.common.base.Splitter; +import com.google.common.collect.Sets; + @ConfigurationProperties(prefix = "saml") public class IamSamlProperties { + @ConfigurationProperties(prefix = "saml.jit-account-provisioning") + public static class IamSamlJITAccountProvisioningProperties { + + private Boolean enabled = FALSE; + private String trustedIdps = "all"; + private Boolean cleanupTaskEnabled = FALSE; + + @Min(5) + private long cleanupTaskPeriodSec = TimeUnit.DAYS.toSeconds(1); + + @Min(1) + private Integer inactiveAccountLifetimeDays = 15; + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public String getTrustedIdps() { + return trustedIdps; + } + + public void setTrustedIdps(String trustedIdps) { + this.trustedIdps = trustedIdps; + } + + public Boolean getCleanupTaskEnabled() { + return cleanupTaskEnabled; + } + + public void setCleanupTaskEnabled(Boolean cleanupEnabled) { + this.cleanupTaskEnabled = cleanupEnabled; + } + + public Integer getInactiveAccountLifetimeDays() { + return inactiveAccountLifetimeDays; + } + + public void setInactiveAccountLifetimeDays(Integer inactiveUserLifetimeDays) { + this.inactiveAccountLifetimeDays = inactiveUserLifetimeDays; + } + + public long getCleanupTaskPeriodSec() { + return cleanupTaskPeriodSec; + } + + public void setCleanupTaskPeriodSec(long cleanupTaskPeriodSec) { + this.cleanupTaskPeriodSec = cleanupTaskPeriodSec; + } + + + public Optional> getTrustedIdpsAsOptionalSet() { + if ("all".equals(trustedIdps)) { + return Optional.empty(); + } + + Set trustedIdpIds = + Sets.newHashSet(Splitter.on(",").trimResults().omitEmptyStrings().split(trustedIdps)); + + if (trustedIdpIds.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(trustedIdpIds); + } + } + private String entityId; private String idpMetadata; private String keystore; private String keystorePassword; private String keyId; private String keyPassword; + private String idResolvers; private int maxAssertionTimeSec; private int maxAuthenticationAgeSec; - public IamSamlProperties() {} + private int metadataLookupServiceRefreshPeriodSec = (int) TimeUnit.MINUTES.toSeconds(5); public String getEntityId() { return entityId; @@ -25,7 +107,7 @@ public String getEntityId() { public void setEntityId(String entityId) { this.entityId = entityId; } - + public String getIdpMetadata() { return idpMetadata; } @@ -82,4 +164,20 @@ public void setMaxAuthenticationAgeSec(int maxAuthenticationAgeSec) { this.maxAuthenticationAgeSec = maxAuthenticationAgeSec; } + public String getIdResolvers() { + return idResolvers; + } + + public void setIdResolvers(String idResolvers) { + this.idResolvers = idResolvers; + } + + public int getMetadataLookupServiceRefreshPeriodSec() { + return metadataLookupServiceRefreshPeriodSec; + } + + public void setMetadataLookupServiceRefreshPeriodSec(int metadataLookupServiceRefreshPeriodSec) { + this.metadataLookupServiceRefreshPeriodSec = metadataLookupServiceRefreshPeriodSec; + } + } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/saml/SamlConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/saml/SamlConfig.java index f6956fae0..0bccebc45 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/saml/SamlConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/saml/SamlConfig.java @@ -1,16 +1,20 @@ package it.infn.mw.iam.config.saml; -import static it.infn.mw.iam.authn.saml.util.SamlIdResolvers.eppn; -import static it.infn.mw.iam.authn.saml.util.SamlIdResolvers.epuid; +import static it.infn.mw.iam.authn.saml.util.Saml2Attribute.EPPN; +import static it.infn.mw.iam.authn.saml.util.Saml2Attribute.EPTID; +import static it.infn.mw.iam.authn.saml.util.Saml2Attribute.EPUID; +import java.io.File; import java.io.IOException; import java.net.URISyntaxException; +import java.nio.file.Files; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Timer; +import java.util.concurrent.TimeUnit; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; @@ -18,12 +22,18 @@ import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; import org.apache.velocity.app.VelocityEngine; import org.opensaml.saml2.core.NameIDType; +import org.opensaml.saml2.metadata.provider.FileBackedHTTPMetadataProvider; import org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider; import org.opensaml.saml2.metadata.provider.MetadataProvider; import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.opensaml.saml2.metadata.provider.ResourceBackedMetadataProvider; +import org.opensaml.util.resource.ClasspathResource; +import org.opensaml.util.resource.ResourceException; import org.opensaml.xml.parse.BasicParserPool; import org.opensaml.xml.parse.ParserPool; import org.opensaml.xml.parse.StaticBasicParserPool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -36,6 +46,9 @@ import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @@ -94,26 +107,39 @@ import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; + import it.infn.mw.iam.authn.ExternalAuthenticationFailureHandler; import it.infn.mw.iam.authn.ExternalAuthenticationSuccessHandler; import it.infn.mw.iam.authn.InactiveAccountAuthenticationHander; import it.infn.mw.iam.authn.RootIsDashboardSuccessHandler; import it.infn.mw.iam.authn.TimestamperSuccessHandler; +import it.infn.mw.iam.authn.saml.CleanInactiveProvisionedAccounts; import it.infn.mw.iam.authn.saml.DefaultSAMLUserDetailsService; import it.infn.mw.iam.authn.saml.IamSamlAuthenticationProvider; +import it.infn.mw.iam.authn.saml.JustInTimeProvisioningSAMLUserDetailsService; +import it.infn.mw.iam.authn.saml.MetadataLookupService; import it.infn.mw.iam.authn.saml.SamlExceptionMessageHelper; import it.infn.mw.iam.authn.saml.util.FirstApplicableChainedSamlIdResolver; +import it.infn.mw.iam.authn.saml.util.SamlIdResolvers; import it.infn.mw.iam.authn.saml.util.SamlUserIdentifierResolver; +import it.infn.mw.iam.config.saml.IamSamlProperties.IamSamlJITAccountProvisioningProperties; import it.infn.mw.iam.config.saml.SamlConfig.IamProperties; import it.infn.mw.iam.config.saml.SamlConfig.ServerProperties; +import it.infn.mw.iam.core.time.SystemTimeProvider; +import it.infn.mw.iam.core.user.IamAccountService; import it.infn.mw.iam.persistence.repository.IamAccountRepository; @Configuration @Order(value = Ordered.LOWEST_PRECEDENCE) @Profile("saml") -@EnableConfigurationProperties({IamSamlProperties.class, IamProperties.class, - ServerProperties.class}) -public class SamlConfig extends WebSecurityConfigurerAdapter { +@EnableConfigurationProperties({IamSamlProperties.class, + IamSamlJITAccountProvisioningProperties.class, IamProperties.class, ServerProperties.class}) +@EnableScheduling +public class SamlConfig extends WebSecurityConfigurerAdapter implements SchedulingConfigurer { + + public static final Logger LOG = LoggerFactory.getLogger(SamlConfig.class); @Autowired ResourceLoader resourceLoader; @@ -124,18 +150,32 @@ public class SamlConfig extends WebSecurityConfigurerAdapter { @Autowired IamAccountRepository repo; + @Autowired + IamAccountService accountService; + @Autowired IamProperties iamProperties; @Autowired IamSamlProperties samlProperties; + @Autowired + IamSamlJITAccountProvisioningProperties jitProperties; + @Autowired ServerProperties serverProperties; @Autowired InactiveAccountAuthenticationHander inactiveAccountHandler; + @Autowired + MetadataLookupService metadataLookupService; + + + Timer metadataFetchTimer = new Timer(); + + BasicParserPool basicParserPool = new BasicParserPool(); + @ConfigurationProperties(prefix = "iam") public static class IamProperties { @@ -165,12 +205,46 @@ public void setUseForwardHeaders(boolean useForwardHeaders) { } @Configuration + @EnableConfigurationProperties({IamSamlProperties.class}) public static class IamSamlConfig { + + protected static final String[] DEFAULT_ID_RESOLVERS = + {EPUID.getAlias(), EPTID.getAlias(), EPPN.getAlias()}; + + @Autowired + IamSamlProperties samlProperties; + + private String[] resolverNames() { + + if (Strings.isNullOrEmpty(samlProperties.getIdResolvers())) { + return DEFAULT_ID_RESOLVERS; + } + + return samlProperties.getIdResolvers().split(","); + } + @Bean public SamlUserIdentifierResolver resolver() { - List resolvers = Arrays.asList(epuid(), eppn()); + List resolvers = new ArrayList<>(); + + SamlIdResolvers resolverFactory = new SamlIdResolvers(); + + String[] resolverNames = resolverNames(); + + for (String n : resolverNames) { + SamlUserIdentifierResolver r = resolverFactory.byName(n); + if (r != null) { + resolvers.add(r); + } else { + LOG.warn("Unsupported saml id resolver: {}", n); + } + } + + if (resolvers.isEmpty()) { + throw new IllegalStateException("Could not configure SAML id resolvers"); + } return new FirstApplicableChainedSamlIdResolver(resolvers); } @@ -181,6 +255,12 @@ public SamlUserIdentifierResolver resolver() { public SAMLUserDetailsService samlUserDetailsService(SamlUserIdentifierResolver resolver, IamAccountRepository accountRepo, InactiveAccountAuthenticationHander handler) { + if (jitProperties.getEnabled()) { + + return new JustInTimeProvisioningSAMLUserDetailsService(resolver, accountService, handler, + accountRepo, jitProperties.getTrustedIdpsAsOptionalSet()); + } + return new DefaultSAMLUserDetailsService(resolver, accountRepo, handler); } @@ -217,13 +297,13 @@ public HttpClient httpClient() { return new HttpClient(multiThreadedHttpConnectionManager()); } - // SAML Authentication Provider responsible for validating of received SAML - // messages @Bean public SAMLAuthenticationProvider samlAuthenticationProvider(SamlUserIdentifierResolver resolver, IamAccountRepository accountRepo, InactiveAccountAuthenticationHander handler) { - IamSamlAuthenticationProvider samlAuthenticationProvider = new IamSamlAuthenticationProvider(); + IamSamlAuthenticationProvider samlAuthenticationProvider = + new IamSamlAuthenticationProvider(resolver); + samlAuthenticationProvider .setUserDetails(samlUserDetailsService(resolver, accountRepo, handler)); samlAuthenticationProvider.setForcePrincipalAsString(false); @@ -302,7 +382,7 @@ public SingleLogoutProfile logoutprofile() { @Bean public KeyManager keyManager() { - Map passwords = new HashMap(); + Map passwords = new HashMap<>(); passwords.put(samlProperties.getKeyId(), samlProperties.getKeyPassword()); DefaultResourceLoader loader = new DefaultResourceLoader(); @@ -332,17 +412,6 @@ public Protocol socketFactoryProtocol() { return new Protocol("https", socketFactory(), 443); } - // - // @Bean - // public MethodInvokingFactoryBean socketFactoryInitialization() { - // - // MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean(); - // methodInvokingFactoryBean.setTargetClass(Protocol.class); - // methodInvokingFactoryBean.setTargetMethod("registerProtocol"); - // Object[] args = {"https", socketFactoryProtocol()}; - // methodInvokingFactoryBean.setArguments(args); - // return methodInvokingFactoryBean; - // } @Bean public WebSSOProfileOptions defaultWebSSOProfileOptions() { @@ -365,7 +434,6 @@ public SAMLEntryPoint samlEntryPoint() { return samlEntryPoint; } - // Setup advanced info about metadata @Bean public ExtendedMetadata extendedMetadata() { @@ -378,40 +446,88 @@ public ExtendedMetadata extendedMetadata() { return extendedMetadata; } - @Bean - @Qualifier("local") - public ExtendedMetadataDelegate localIdpMetadataProvider() - throws MetadataProviderException, URISyntaxException, IOException { - - Resource metadataResource = resourceLoader.getResource(samlProperties.getIdpMetadata()); - - FilesystemMetadataProvider localIdpMetadataProvider = - new FilesystemMetadataProvider(metadataResource.getFile()); - - localIdpMetadataProvider.setParserPool(new BasicParserPool()); - localIdpMetadataProvider.initialize(); - + private ExtendedMetadataDelegate metadataDelegate(MetadataProvider p) { ExtendedMetadataDelegate extendedMetadataDelegate = - new ExtendedMetadataDelegate(localIdpMetadataProvider, extendedMetadata()); + new ExtendedMetadataDelegate(p, extendedMetadata()); extendedMetadataDelegate.setMetadataTrustCheck(true); extendedMetadataDelegate.setMetadataRequireSignature(false); return extendedMetadataDelegate; + } + + private List metadataProviders() + throws MetadataProviderException, IOException, ResourceException { + + List providers = new ArrayList<>(); + + String metadata = samlProperties.getIdpMetadata(); + Iterable metadataIterable = + Splitter.on(",").trimResults().omitEmptyStrings().split(metadata); + + + for (String m : metadataIterable) { + if (m.startsWith("classpath:")) { + LOG.info("Adding classpath based metadata provider for URL: {}", m); + + ClasspathResource cpMetadataResources = + new ClasspathResource(m.replaceFirst("classpath:", "")); + + ResourceBackedMetadataProvider metadataProvider = + new ResourceBackedMetadataProvider(metadataFetchTimer, cpMetadataResources); + + metadataProvider.setParserPool(basicParserPool); + metadataProvider.initialize(); + providers.add(metadataDelegate(metadataProvider)); + + } else if (m.startsWith("file:")) { + + LOG.info("Adding File based metadata provider for URL: {}", m); + Resource metadataResource = resourceLoader.getResource(m); + + + FilesystemMetadataProvider metadataProvider = + new FilesystemMetadataProvider(metadataResource.getFile()); + + metadataProvider.setParserPool(basicParserPool); + metadataProvider.initialize(); + providers.add(metadataDelegate(metadataProvider)); + + } else if (m.startsWith("http")) { + + LOG.info("Adding HTTP metadata provider for URL: {}", m); + + File metadataBackupFile = Files.createTempFile("metadata", "xml").toFile(); + metadataBackupFile.deleteOnExit(); + + FileBackedHTTPMetadataProvider metadataProvider = new FileBackedHTTPMetadataProvider( + metadataFetchTimer, httpClient(), m, metadataBackupFile.getAbsolutePath()); + + metadataProvider.setParserPool(basicParserPool); + metadataProvider.initialize(); + providers.add(metadataDelegate(metadataProvider)); + } else { + LOG.error("Skipping invalid saml.idp-metatadata value: {}", m); + } + } + + if (providers.isEmpty()) { + String message = "Empty SAML metadata providers after initialization"; + LOG.error(message); + throw new IllegalStateException(message); + } + + return providers; } - // IDP Metadata configuration - paths to metadata of IDPs in circle of trust - // is here + @Bean @Qualifier("metadata") public CachingMetadataManager metadata() - throws MetadataProviderException, URISyntaxException, IOException { + throws MetadataProviderException, URISyntaxException, IOException, ResourceException { - List providers = new ArrayList(); - providers.add(localIdpMetadataProvider()); - return new CachingMetadataManager(providers); + return new CachingMetadataManager(metadataProviders()); } - // Filter automatically generates default SP metadata @Bean public MetadataGenerator metadataGenerator() { @@ -435,15 +551,12 @@ public MetadataDisplayFilter metadataDisplayFilter() { @Bean - public AuthenticationSuccessHandler successRedirectHandler() { - + public AuthenticationSuccessHandler samlAuthenticationSuccessHandler() { RootIsDashboardSuccessHandler sa = new RootIsDashboardSuccessHandler(iamProperties.getBaseUrl(), new HttpSessionRequestCache()); - ExternalAuthenticationSuccessHandler successHandler = - new ExternalAuthenticationSuccessHandler(new TimestamperSuccessHandler(sa), "/"); - return successHandler; + return new ExternalAuthenticationSuccessHandler(new TimestamperSuccessHandler(sa, repo), "/"); } @@ -457,7 +570,8 @@ public SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter() throws Exce SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter = new SAMLWebSSOHoKProcessingFilter(); - samlWebSSOHoKProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler()); + samlWebSSOHoKProcessingFilter + .setAuthenticationSuccessHandler(samlAuthenticationSuccessHandler()); samlWebSSOHoKProcessingFilter.setAuthenticationManager(authenticationManager()); samlWebSSOHoKProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler()); return samlWebSSOHoKProcessingFilter; @@ -469,7 +583,7 @@ public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception { SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter(); samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManager()); - samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler()); + samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(samlAuthenticationSuccessHandler()); samlWebSSOProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler()); return samlWebSSOProcessingFilter; } @@ -558,7 +672,7 @@ public HTTPPAOS11Binding httpPAOS11Binding() { @Bean public SAMLProcessor processor() { - Collection bindings = new ArrayList(); + Collection bindings = new ArrayList<>(); bindings.add(httpRedirectDeflateBinding()); bindings.add(httpPostBinding()); bindings.add(artifactBinding(parserPool(), velocityEngine())); @@ -576,7 +690,7 @@ public SAMLProcessor processor() { @Bean public FilterChainProxy samlFilter() throws Exception { - List chains = new ArrayList(); + List chains = new ArrayList<>(); chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"), samlEntryPoint())); chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"), @@ -595,12 +709,13 @@ public FilterChainProxy samlFilter() throws Exception { @Override protected void configure(HttpSecurity http) throws Exception { + String pattern = "/saml/**"; - http.antMatcher("/saml/**"); + http.antMatcher(pattern); - http.csrf().ignoringAntMatchers("/saml/**"); + http.csrf().ignoringAntMatchers(pattern); - http.authorizeRequests().antMatchers("/saml/**").permitAll(); + http.authorizeRequests().antMatchers(pattern).permitAll(); http.addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class) .addFilterAfter(samlFilter(), BasicAuthenticationFilter.class); @@ -611,4 +726,42 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(samlAuthenticationProvider(resolver, repo, inactiveAccountHandler)); } + + private void scheduleMetadataLookupServiceRefresh(ScheduledTaskRegistrar taskRegistrar) { + LOG.info("Scheduling metadata lookup service refresh task to run every {} seconds.", + samlProperties.getMetadataLookupServiceRefreshPeriodSec()); + taskRegistrar.addFixedRateTask(() -> metadataLookupService.refreshMetadata(), + TimeUnit.SECONDS.toMillis(samlProperties.getMetadataLookupServiceRefreshPeriodSec())); + } + + private void scheduleProvisionedAccountsCleanup(final ScheduledTaskRegistrar taskRegistrar) { + + if (!jitProperties.getEnabled()) { + LOG.info("Just-in-time account provisioning for SAML is DISABLED."); + return; + } + + if (!jitProperties.getCleanupTaskEnabled()) { + LOG.info("Cleanup for SAML JIT account provisioning is DISABLED."); + return; + } + + LOG.info( + "Scheduling Just-in-time provisioned account cleanup task to run every {} seconds. Accounts inactive for {} " + + "days will be deleted", + jitProperties.getCleanupTaskPeriodSec(), jitProperties.getInactiveAccountLifetimeDays()); + + taskRegistrar.addFixedRateTask( + new CleanInactiveProvisionedAccounts(new SystemTimeProvider(), accountService, + jitProperties.getInactiveAccountLifetimeDays()), + TimeUnit.SECONDS.toMillis(jitProperties.getCleanupTaskPeriodSec())); + + } + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + scheduleProvisionedAccountsCleanup(taskRegistrar); + scheduleMetadataLookupServiceRefresh(taskRegistrar); + } + } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/IamIntrospectionResultAssembler.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/IamIntrospectionResultAssembler.java index a97c53527..7d32390d8 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/IamIntrospectionResultAssembler.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/IamIntrospectionResultAssembler.java @@ -27,8 +27,6 @@ public class IamIntrospectionResultAssembler extends DefaultIntrospectionResultA public static final String GROUPS = "groups"; public static final String ORGANISATION_NAME = "organisation_name"; - public IamIntrospectionResultAssembler() {} - @Override public Map assembleFrom(OAuth2AccessTokenEntity accessToken, UserInfo userInfo, Set authScopes) { @@ -39,7 +37,7 @@ public Map assembleFrom(OAuth2AccessTokenEntity accessToken, Use List audience = accessToken.getJwt().getJWTClaimsSet().getAudience(); - if (audience != null && audience.size() > 0) { + if (audience != null && !audience.isEmpty()) { result.put("aud", Joiner.on(' ').join(audience)); } @@ -64,6 +62,8 @@ public Map assembleFrom(OAuth2AccessTokenEntity accessToken, Use result.put(PREFERRED_USERNAME, iamUserInfo.getPreferredUsername()); + LOGGER.debug("Organisation name: {}", IamProperties.INSTANCE.getOrganisationName()); + result.put(ORGANISATION_NAME, IamProperties.INSTANCE.getOrganisationName()); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/IamOAuth2RequestFactory.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/IamOAuth2RequestFactory.java index 9812f00a1..db52bafa9 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/IamOAuth2RequestFactory.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/IamOAuth2RequestFactory.java @@ -9,7 +9,7 @@ public class IamOAuth2RequestFactory extends ConnectOAuth2RequestFactory { - public static final String[] AUDIENCE_KEYS = {"aud", "audience"}; + protected static final String[] AUDIENCE_KEYS = {"aud", "audience"}; public static final String AUD = "aud"; public static final String PASSWORD_GRANT = "password"; diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/IamScopeClaimTranslationService.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/IamScopeClaimTranslationService.java index 8d38ec40c..6fb596136 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/IamScopeClaimTranslationService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/IamScopeClaimTranslationService.java @@ -16,36 +16,42 @@ public class IamScopeClaimTranslationService implements ScopeClaimTranslationSer private SetMultimap scopesToClaims = HashMultimap.create(); + public static final String OPENID_SCOPE = "openid"; + public static final String PROFILE_SCOPE = "profile"; + public static final String EMAIL_SCOPE = "email"; + public static final String PHONE_SCOPE = "phone"; + public static final String ADDRESS_SCOPE = "address"; + public IamScopeClaimTranslationService() { // Mitreid scope mappings - scopesToClaims.put("openid", "sub"); - - scopesToClaims.put("profile", "name"); - scopesToClaims.put("profile", "preferred_username"); - scopesToClaims.put("profile", "given_name"); - scopesToClaims.put("profile", "family_name"); - scopesToClaims.put("profile", "middle_name"); - scopesToClaims.put("profile", "nickname"); - scopesToClaims.put("profile", "profile"); - scopesToClaims.put("profile", "picture"); - scopesToClaims.put("profile", "website"); - scopesToClaims.put("profile", "gender"); - scopesToClaims.put("profile", "zoneinfo"); - scopesToClaims.put("profile", "locale"); - scopesToClaims.put("profile", "updated_at"); - scopesToClaims.put("profile", "birthdate"); - - scopesToClaims.put("email", "email"); - scopesToClaims.put("email", "email_verified"); - - scopesToClaims.put("phone", "phone_number"); - scopesToClaims.put("phone", "phone_number_verified"); - - scopesToClaims.put("address", "address"); + scopesToClaims.put(OPENID_SCOPE, "sub"); + + scopesToClaims.put(PROFILE_SCOPE, "name"); + scopesToClaims.put(PROFILE_SCOPE, "preferred_username"); + scopesToClaims.put(PROFILE_SCOPE, "given_name"); + scopesToClaims.put(PROFILE_SCOPE, "family_name"); + scopesToClaims.put(PROFILE_SCOPE, "middle_name"); + scopesToClaims.put(PROFILE_SCOPE, "nickname"); + scopesToClaims.put(PROFILE_SCOPE, "profile"); + scopesToClaims.put(PROFILE_SCOPE, "picture"); + scopesToClaims.put(PROFILE_SCOPE, "website"); + scopesToClaims.put(PROFILE_SCOPE, "gender"); + scopesToClaims.put(PROFILE_SCOPE, "zoneinfo"); + scopesToClaims.put(PROFILE_SCOPE, "locale"); + scopesToClaims.put(PROFILE_SCOPE, "updated_at"); + scopesToClaims.put(PROFILE_SCOPE, "birthdate"); + + scopesToClaims.put(EMAIL_SCOPE, "email"); + scopesToClaims.put(EMAIL_SCOPE, "email_verified"); + + scopesToClaims.put(PHONE_SCOPE, "phone_number"); + scopesToClaims.put(PHONE_SCOPE, "phone_number_verified"); + + scopesToClaims.put(ADDRESS_SCOPE, "address"); // Iam scope mappings - scopesToClaims.put("profile", "organisation_name"); - scopesToClaims.put("profile", "groups"); + scopesToClaims.put(PROFILE_SCOPE, "organisation_name"); + scopesToClaims.put(PROFILE_SCOPE, "groups"); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/IamTokenService.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/IamTokenService.java index 484b71cdf..7122c5b76 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/IamTokenService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/IamTokenService.java @@ -1,5 +1,6 @@ package it.infn.mw.iam.core; +import java.util.Date; import java.util.Set; import org.mitre.oauth2.model.OAuth2AccessTokenEntity; @@ -33,7 +34,7 @@ public IamTokenService(IamOAuthAccessTokenRepository atRepo, public Set getAllAccessTokensForUser(String id) { Set results = Sets.newLinkedHashSet(); - results.addAll(accessTokenRepo.findValidAccessTokensForUser(id)); + results.addAll(accessTokenRepo.findValidAccessTokensForUser(id, new Date())); return results; } @@ -41,7 +42,7 @@ public Set getAllAccessTokensForUser(String id) { @Override public Set getAllRefreshTokensForUser(String id) { Set results = Sets.newLinkedHashSet(); - results.addAll(refreshTokenRepo.findValidRefreshTokensForUser(id)); + results.addAll(refreshTokenRepo.findValidRefreshTokensForUser(id, new Date())); return results; } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/IamUserDetailsService.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/IamUserDetailsService.java index 5d44285f8..557a51cd0 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/IamUserDetailsService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/IamUserDetailsService.java @@ -43,8 +43,7 @@ public UserDetails loadUserByUsername(final String username) throws UsernameNotF if (a.isActive()) { - User u = new User(a.getUsername(), a.getPassword(), convertAuthorities(a)); - return u; + return new User(a.getUsername(), a.getPassword(), convertAuthorities(a)); } else { throw new DisabledException("User '" + username + "' is not active."); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/time/SystemTimeProvider.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/time/SystemTimeProvider.java index 60d6b4323..bef2f52fe 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/time/SystemTimeProvider.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/time/SystemTimeProvider.java @@ -11,8 +11,6 @@ @Component public class SystemTimeProvider implements TimeProvider { - public SystemTimeProvider() {} - @Override public long currentTimeMillis() { diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/time/TimeProvider.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/time/TimeProvider.java index 8595ba128..2d636d9b1 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/time/TimeProvider.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/time/TimeProvider.java @@ -4,6 +4,7 @@ * Time provider interface. * */ +@FunctionalInterface public interface TimeProvider { /** diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/user/DefaultIamAccountService.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/user/DefaultIamAccountService.java new file mode 100644 index 000000000..f72454eb9 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/user/DefaultIamAccountService.java @@ -0,0 +1,241 @@ +package it.infn.mw.iam.core.user; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; + +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import javax.transaction.Transactional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import it.infn.mw.iam.audit.events.account.AccountCreatedEvent; +import it.infn.mw.iam.audit.events.account.AccountRemovedEvent; +import it.infn.mw.iam.core.user.exception.CredentialAlreadyBoundException; +import it.infn.mw.iam.core.user.exception.InvalidCredentialException; +import it.infn.mw.iam.core.user.exception.UserAlreadyExistsException; +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamAuthority; +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 +@Transactional +public class DefaultIamAccountService implements IamAccountService { + + private final IamAccountRepository accountRepo; + private final IamAuthoritiesRepository authoritiesRepo; + private final PasswordEncoder passwordEncoder; + private final ApplicationEventPublisher eventPublisher; + + @Autowired + public DefaultIamAccountService(IamAccountRepository accountRepo, + IamAuthoritiesRepository authoritiesRepo, PasswordEncoder passwordEncoder, + ApplicationEventPublisher eventPublisher) { + + this.accountRepo = accountRepo; + this.authoritiesRepo = authoritiesRepo; + this.passwordEncoder = passwordEncoder; + this.eventPublisher = eventPublisher; + } + + @Override + public IamAccount createAccount(IamAccount account) { + checkNotNull(account, "Cannot create a null account"); + + final Date now = new Date(); + final String randomUuid = UUID.randomUUID().toString(); + + newAccountSanityChecks(account); + + if (account.getCreationTime() == null) { + account.setCreationTime(now); + } + + if (account.getUuid() == null) { + account.setUuid(randomUuid); + } + + account.setLastUpdateTime(now); + + account.getUserInfo().setEmailVerified(true); + + if (account.getPassword() == null) { + account.setPassword(UUID.randomUUID().toString()); + } + + account.setPassword(passwordEncoder.encode(account.getPassword())); + + IamAuthority roleUserAuthority = authoritiesRepo.findByAuthority("ROLE_USER").orElseThrow( + () -> new IllegalStateException("ROLE_USER not found in database. This is a bug")); + + account.getAuthorities().add(roleUserAuthority); + + // Credentials sanity checks + newAccountX509CertificatesSanityChecks(account); + newAccountSshKeysSanityChecks(account); + newAccountSamlIdsSanityChecks(account); + newAccountOidcIdsSanityChecks(account); + + // Set creation time for certificates + account.getX509Certificates().forEach(c -> { + c.setCreationTime(now); + c.setLastUpdateTime(now); + }); + + accountRepo.save(account); + + eventPublisher.publishEvent(new AccountCreatedEvent(this, account, + "Account created for user " + account.getUsername())); + + return account; + } + + @Override + public IamAccount deleteAccount(IamAccount account) { + checkNotNull(account, "cannot delete a null account"); + accountRepo.delete(account); + + eventPublisher.publishEvent(new AccountRemovedEvent(this, account, + "Removed account for user " + account.getUsername())); + + return account; + } + + private void newAccountOidcIdsSanityChecks(IamAccount account) { + account.getOidcIds().forEach(this::oidcIdSanityChecks); + } + + + private void newAccountSamlIdsSanityChecks(IamAccount account) { + account.getSamlIds().forEach(this::samlIdSanityChecks); + } + + private void newAccountSanityChecks(IamAccount account) { + checkArgument(!isNullOrEmpty(account.getUsername()), "Null or empty username"); + checkNotNull(account.getUserInfo(), "Null userinfo object"); + checkArgument(!isNullOrEmpty(account.getUserInfo().getEmail()), "Null or empty email"); + + accountRepo.findByUsername(account.getUsername()).ifPresent(a -> { + throw new UserAlreadyExistsException( + String.format("A user with username '%s' already exists", a.getUsername())); + }); + + accountRepo.findByEmail(account.getUserInfo().getEmail()).ifPresent(a -> { + throw new UserAlreadyExistsException(String + .format("A user linked with email '%s' already exists", a.getUserInfo().getEmail())); + }); + + } + + private void newAccountSshKeysSanityChecks(IamAccount account) { + + if (account.hasSshKeys()) { + + account.getSshKeys().forEach(this::sshKeySanityChecks); + + final long count = account.getSshKeys().stream().filter(IamSshKey::isPrimary).count(); + + if (count > 1) { + throw new InvalidCredentialException("Only one SSH key can be marked as primary"); + } + + if (count == 0) { + account.getSshKeys().stream().findFirst().ifPresent(k -> k.setPrimary(true)); + } + } + } + + private void newAccountX509CertificatesSanityChecks(IamAccount account) { + + if (account.hasX509Certificates()) { + + account.getX509Certificates().forEach(this::x509CertificateSanityCheck); + + final long count = + account.getX509Certificates().stream().filter(IamX509Certificate::isPrimary).count(); + + if (count > 1) { + throw new InvalidCredentialException("Only one X.509 certificate can be marked as primary"); + } + + if (count == 0) { + account.getX509Certificates().stream().findFirst().ifPresent(c -> c.setPrimary(true)); + } + } + + } + + private void oidcIdSanityChecks(IamOidcId oidcId) { + checkNotNull(oidcId, "null oidc id"); + checkArgument(!isNullOrEmpty(oidcId.getIssuer()), "null or empty oidc id issuer"); + checkArgument(!isNullOrEmpty(oidcId.getSubject()), "null or empty oidc id subject"); + + accountRepo.findByOidcId(oidcId.getIssuer(), oidcId.getSubject()).ifPresent(account -> { + + throw new CredentialAlreadyBoundException(String.format( + "OIDC id '%s,%s' is already bound to a user", oidcId.getIssuer(), oidcId.getSubject())); + }); + } + + private void samlIdSanityChecks(IamSamlId samlId) { + + checkNotNull(samlId, "null saml id"); + + checkArgument(!isNullOrEmpty(samlId.getIdpId()), "null or empty idpId"); + checkArgument(!isNullOrEmpty(samlId.getUserId()), "null or empty userId"); + checkArgument(!isNullOrEmpty(samlId.getAttributeId()), "null or empty attributeId"); + + accountRepo.findBySamlId(samlId).ifPresent(account -> { + throw new CredentialAlreadyBoundException( + String.format("SAML id '%s,%s,%s' already bound to a user", samlId.getIdpId(), + samlId.getAttributeId(), samlId.getUserId())); + }); + } + + private void sshKeySanityChecks(IamSshKey sshKey) { + + checkNotNull(sshKey, "null ssh key"); + checkArgument(!isNullOrEmpty(sshKey.getValue()), "null or empty ssh key value"); + + accountRepo.findBySshKeyValue(sshKey.getValue()).ifPresent(account -> { + throw new CredentialAlreadyBoundException( + String.format("SSH key '%s' already bound to a user", sshKey.getValue())); + }); + } + + private void x509CertificateSanityCheck(IamX509Certificate cert) { + checkNotNull(cert, "null X.509 certificate"); + checkArgument(!isNullOrEmpty(cert.getSubjectDn()), + "null or empty X.509 certificate subject DN"); + checkArgument(!isNullOrEmpty(cert.getIssuerDn()), "null or empty X.509 certificate issuer DN"); + checkArgument(!isNullOrEmpty(cert.getLabel()), "null or empty X.509 certificate label"); + + accountRepo.findByCertificateSubject(cert.getSubjectDn()).ifPresent(c -> { + throw new CredentialAlreadyBoundException(String + .format("X509 certificate with subject '%s' is already bound to another user", cert.getSubjectDn())); + }); + } + + @Override + public List deleteInactiveProvisionedUsersSinceTime(Date timestamp) { + checkNotNull(timestamp, "null timestamp"); + + List accounts = + accountRepo.findProvisionedAccountsWithLastLoginTimeBeforeTimestamp(timestamp); + + accounts.forEach(this::deleteAccount); + + return accounts; + } +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/user/IamAccountService.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/user/IamAccountService.java new file mode 100644 index 000000000..e416ebc2a --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/user/IamAccountService.java @@ -0,0 +1,39 @@ +package it.infn.mw.iam.core.user; + +import java.util.Date; +import java.util.List; + +import it.infn.mw.iam.persistence.model.IamAccount; + +/** + * This service provides basic functionality used to manage IAM accounts + */ +public interface IamAccountService { + + /** + * Creates a new {@link IamAccount}, after some checks. + * + * @param account the account to be created + * @return the created {@link IamAccount} + */ + IamAccount createAccount(IamAccount account); + + + /** + * Deletes a {@link IamAccount}. + * + * @param account the account to be deleted + * + * @return the deleted {@link IamAccount} + */ + IamAccount deleteAccount(IamAccount account); + + /** + * Deletes provisioned accounts whose last login time is before than the timestamp passed as + * argument + * + * @param timestamp the timestamp + * @return the possibly empty {@link List} of {@link IamAccount} that have been removed + */ + List deleteInactiveProvisionedUsersSinceTime(Date timestamp); +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/user/exception/CredentialAlreadyBoundException.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/user/exception/CredentialAlreadyBoundException.java new file mode 100644 index 000000000..9f8cebb7c --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/user/exception/CredentialAlreadyBoundException.java @@ -0,0 +1,14 @@ +package it.infn.mw.iam.core.user.exception; + +public class CredentialAlreadyBoundException extends IamAccountException { + + /** + * + */ + private static final long serialVersionUID = -5213327060856570097L; + + public CredentialAlreadyBoundException(String message) { + super(message); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/user/exception/IamAccountException.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/user/exception/IamAccountException.java new file mode 100644 index 000000000..99155f5c6 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/user/exception/IamAccountException.java @@ -0,0 +1,14 @@ +package it.infn.mw.iam.core.user.exception; + +public class IamAccountException extends RuntimeException { + + /** + * + */ + private static final long serialVersionUID = 2769590935871518008L; + + public IamAccountException(String message) { + super(message); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/user/exception/InvalidCredentialException.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/user/exception/InvalidCredentialException.java new file mode 100644 index 000000000..c0773f0cb --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/user/exception/InvalidCredentialException.java @@ -0,0 +1,11 @@ +package it.infn.mw.iam.core.user.exception; + +public class InvalidCredentialException extends IamAccountException { + + private static final long serialVersionUID = 7461872494570748516L; + + public InvalidCredentialException(String message) { + super(message); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/user/exception/UserAlreadyExistsException.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/user/exception/UserAlreadyExistsException.java new file mode 100644 index 000000000..fb749716a --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/user/exception/UserAlreadyExistsException.java @@ -0,0 +1,14 @@ +package it.infn.mw.iam.core.user.exception; + +public class UserAlreadyExistsException extends IamAccountException { + + /** + * + */ + private static final long serialVersionUID = 4103663720620113509L; + + public UserAlreadyExistsException(String message) { + super(message); + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/util/AuthenticationLogger.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/util/AuthenticationLogger.java index 7aa012888..4d9ff7b27 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/util/AuthenticationLogger.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/util/AuthenticationLogger.java @@ -2,6 +2,7 @@ import org.springframework.security.core.Authentication; +@FunctionalInterface public interface AuthenticationLogger { public void logAuthenticationSuccess(Authentication auth); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/util/IamAuthenticationLogger.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/util/IamAuthenticationLogger.java index 134c03f87..1ac9ad943 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/util/IamAuthenticationLogger.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/util/IamAuthenticationLogger.java @@ -9,12 +9,12 @@ public enum IamAuthenticationLogger implements AuthenticationLogger { INSTANCE; - private final Logger LOG = LoggerFactory.getLogger(IamAuthenticationLogger.class); + private final Logger log = LoggerFactory.getLogger(IamAuthenticationLogger.class); @Override public void logAuthenticationSuccess(Authentication auth) { if (!(auth instanceof OAuth2Authentication)) { - LOG.info("{} was authenticated succesfully", auth.getName()); + log.info("{} was authenticated succesfully", auth.getName()); return; } @@ -23,9 +23,9 @@ public void logAuthenticationSuccess(Authentication auth) { if (oauth.getUserAuthentication() != null) { final String userName = oauth.getUserAuthentication().getName(); - LOG.info("{} acting for {} was authenticated succesfully", clientName, userName); + log.info("{} acting for {} was authenticated succesfully", clientName, userName); } else { - LOG.info("Client {} was authenticated succesfully", clientName); + log.info("Client {} was authenticated succesfully", clientName); } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/util/PoliteJsonMessageSource.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/util/PoliteJsonMessageSource.java index ce113daf4..593816293 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/util/PoliteJsonMessageSource.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/util/PoliteJsonMessageSource.java @@ -33,7 +33,7 @@ public class PoliteJsonMessageSource extends AbstractMessageSource { // Logger for this class - private static final Logger logger = LoggerFactory.getLogger(PoliteJsonMessageSource.class); + private static final Logger LOG = LoggerFactory.getLogger(PoliteJsonMessageSource.class); private Resource baseDirectory; @@ -148,7 +148,7 @@ private List getLanguageMap(Locale locale) { Resource r = getBaseDirectory().createRelative(filename); - logger.info("No locale loaded, trying to load from " + r); + LOG.info("No locale loaded, trying to load from {}" , r); JsonParser parser = new JsonParser(); JsonObject obj = @@ -158,7 +158,7 @@ private List getLanguageMap(Locale locale) { } languageMaps.put(locale, set); } catch (JsonIOException | JsonSyntaxException | IOException e) { - logger.debug("Unable to load locale: {}", e.getMessage()); + LOG.debug("Unable to load locale: {}", e.getMessage(),e); } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/web/DefaultLoginPageConfiguration.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/web/DefaultLoginPageConfiguration.java index b1199129c..64a356d3c 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/web/DefaultLoginPageConfiguration.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/web/DefaultLoginPageConfiguration.java @@ -1,10 +1,13 @@ package it.infn.mw.iam.core.web; +import static it.infn.mw.iam.api.account_linking.AccountLinkingConstants.ACCOUNT_LINKING_DISABLE_PROPERTY; + import java.util.Arrays; import javax.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.EnvironmentAware; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; @@ -20,7 +23,9 @@ public class DefaultLoginPageConfiguration implements LoginPageConfiguration, En private boolean githubEnabled; private boolean samlEnabled; private boolean registrationEnabled; - + + @Value(ACCOUNT_LINKING_DISABLE_PROPERTY) + private Boolean accountLinkingDisable; @Autowired GoogleClientProperties googleClientConfiguration; @@ -70,4 +75,9 @@ public boolean isRegistrationEnabled() { return registrationEnabled; } + @Override + public boolean isAccountLinkingEnabled() { + return !accountLinkingDisable.booleanValue(); + } + } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/web/IamDiscoveryEndpoint.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/web/IamDiscoveryEndpoint.java index e9adbb42a..431b21c15 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/web/IamDiscoveryEndpoint.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/web/IamDiscoveryEndpoint.java @@ -81,8 +81,8 @@ public String apply(Algorithm alg) { public String webfinger(@RequestParam("resource") String resource, @RequestParam(value = "rel", required = false) String rel, Model model) { - if (!Strings.isNullOrEmpty(rel) && !rel.equals("http://openid.net/specs/connect/1.0/issuer")) { - logger.warn("Responding to webfinger request for non-OIDC relation: " + rel); + if (!Strings.isNullOrEmpty(rel) && !"http://openid.net/specs/connect/1.0/issuer".equals(rel)) { + logger.warn("Responding to webfinger request for non-OIDC relation: {}", rel); } if (!resource.equals(config.getIssuer())) { @@ -118,7 +118,7 @@ public String webfinger(@RequestParam("resource") String resource, // if the user's still null, punt and say we didn't find them - logger.info("User not found: " + resource); + logger.info("User not found: {}", resource); model.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); return HttpCodeView.VIEWNAME; } @@ -126,7 +126,7 @@ public String webfinger(@RequestParam("resource") String resource, } } else { - logger.info("Unknown URI format: " + resource); + logger.info("Unknown URI format: {}", resource); model.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND); return HttpCodeView.VIEWNAME; } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/web/LoginPageConfiguration.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/web/LoginPageConfiguration.java index 48b837043..e400c3bb8 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/web/LoginPageConfiguration.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/web/LoginPageConfiguration.java @@ -9,4 +9,6 @@ public interface LoginPageConfiguration { boolean isSamlEnabled(); boolean isRegistrationEnabled(); + + boolean isAccountLinkingEnabled(); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/github/GithubAuthFilter.java b/iam-login-service/src/main/java/it/infn/mw/iam/github/GithubAuthFilter.java index b15936dcd..41e9e6b88 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/github/GithubAuthFilter.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/github/GithubAuthFilter.java @@ -8,22 +8,21 @@ import javax.servlet.http.HttpSession; import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter; import org.springframework.security.web.savedrequest.DefaultSavedRequest; public class GithubAuthFilter extends OAuth2ClientAuthenticationProcessingFilter { - protected final static String REDIRECT_URI_SESSION_VARIABLE = "redirect_uri"; - protected final static String STATE_SESSION_VARIABLE = "state"; - protected final static String NONCE_SESSION_VARIABLE = "nonce"; - protected final static String ISSUER_SESSION_VARIABLE = "issuer"; + protected static final String REDIRECT_URI_SESSION_VARIABLE = "redirect_uri"; + protected static final String STATE_SESSION_VARIABLE = "state"; + protected static final String NONCE_SESSION_VARIABLE = "nonce"; + protected static final String ISSUER_SESSION_VARIABLE = "issuer"; protected static final String TARGET_SESSION_VARIABLE = "target"; - protected final static int HTTP_SOCKET_TIMEOUT = 30000; + protected static final int HTTP_SOCKET_TIMEOUT = 30000; - protected final static String ORIGIN_AUTH_REQUEST_SESSION_VARIABLE = "origin_auth_request"; + protected static final String ORIGIN_AUTH_REQUEST_SESSION_VARIABLE = "origin_auth_request"; - public final static String FILTER_PROCESSES_URL = "/login/github"; + public static final String FILTER_PROCESSES_URL = "/login/github"; public GithubAuthFilter() { super(FILTER_PROCESSES_URL); @@ -36,7 +35,7 @@ public GithubAuthFilter(String processingUrl) { @Override public Authentication attemptAuthentication(final HttpServletRequest request, final HttpServletResponse response) - throws AuthenticationException, IOException, ServletException { + throws IOException, ServletException { HttpSession session = request.getSession(); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/notification/DefaultNotificationService.java b/iam-login-service/src/main/java/it/infn/mw/iam/notification/DefaultNotificationService.java index ce86a50f3..db8e0c73d 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/notification/DefaultNotificationService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/notification/DefaultNotificationService.java @@ -134,6 +134,7 @@ public IamEmailNotification createResetPasswordMessage(IamAccount account) { model.put(RECIPIENT_FIELD, recipient); model.put("resetPasswordUrl", resetPasswordUrl); model.put("organisationName", organisationName); + model.put("username", account.getUsername()); return createMessage("resetPassword.vm", model, IamNotificationType.RESETPASSWD, properties.getSubject().get("resetPassword"), account.getUserInfo().getEmail()); @@ -169,7 +170,7 @@ public void sendPendingNotifications() { } catch (MailException me) { elem.setDeliveryStatus(IamDeliveryStatus.DELIVERY_ERROR); logger.error("Message delivery fail. message_id:{} reason:{}", elem.getUuid(), - me.getMessage()); + me.getMessage(), me); } elem.setLastUpdate(new Date()); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/registration/DefaultRegistrationRequestService.java b/iam-login-service/src/main/java/it/infn/mw/iam/registration/DefaultRegistrationRequestService.java index 19bc8c1b6..7e17d8654 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/registration/DefaultRegistrationRequestService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/registration/DefaultRegistrationRequestService.java @@ -23,12 +23,12 @@ import com.google.common.collect.ImmutableTable; import com.google.common.collect.Table; +import it.infn.mw.iam.api.scim.converter.UserConverter; import it.infn.mw.iam.api.scim.exception.IllegalArgumentException; import it.infn.mw.iam.api.scim.exception.ScimResourceNotFoundException; import it.infn.mw.iam.api.scim.model.ScimOidcId; import it.infn.mw.iam.api.scim.model.ScimSamlId; import it.infn.mw.iam.api.scim.model.ScimUser; -import it.infn.mw.iam.api.scim.provisioning.ScimUserProvisioning; import it.infn.mw.iam.audit.events.registration.RegistrationApproveEvent; import it.infn.mw.iam.audit.events.registration.RegistrationConfirmEvent; import it.infn.mw.iam.audit.events.registration.RegistrationRejectEvent; @@ -36,6 +36,7 @@ import it.infn.mw.iam.authn.ExternalAuthenticationRegistrationInfo; import it.infn.mw.iam.authn.ExternalAuthenticationRegistrationInfo.ExternalAuthenticationType; import it.infn.mw.iam.core.IamRegistrationRequestStatus; +import it.infn.mw.iam.core.user.IamAccountService; import it.infn.mw.iam.notification.NotificationService; import it.infn.mw.iam.persistence.model.IamAccount; import it.infn.mw.iam.persistence.model.IamRegistrationRequest; @@ -50,7 +51,10 @@ public class DefaultRegistrationRequestService private IamRegistrationRequestRepository requestRepository; @Autowired - private ScimUserProvisioning userService; + private IamAccountService accountService; + + @Autowired + private UserConverter userConverter; @Autowired @Qualifier("defaultNotificationService") @@ -67,10 +71,6 @@ public class DefaultRegistrationRequestService private ApplicationEventPublisher eventPublisher; - public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { - this.eventPublisher = publisher; - } - private static final Table allowedStateTransitions = new ImmutableTable.Builder() .put(NEW, CONFIRMED, true) @@ -98,6 +98,7 @@ private void addExternalAuthnInfo(ScimUser.Builder user, } else if (ExternalAuthenticationType.SAML.equals(extAuthnInfo.getType())) { ScimSamlId samlId = new ScimSamlId.Builder().idpId(extAuthnInfo.getIssuer()) .userId(extAuthnInfo.getSubject()) + .attributeId(extAuthnInfo.getSubjectAttribute()) .build(); user.addSamlId(samlId); } @@ -107,6 +108,8 @@ private void addExternalAuthnInfo(ScimUser.Builder user, public RegistrationRequestDto createRequest(RegistrationRequestDto request, Optional extAuthnInfo) { + notesSanityChecks(request.getNotes()); + ScimUser.Builder userBuilder = ScimUser.builder() .buildName(request.getGivenname(), request.getFamilyname()) .buildEmail(request.getEmail()) @@ -115,7 +118,8 @@ public RegistrationRequestDto createRequest(RegistrationRequestDto request, extAuthnInfo.ifPresent(i -> addExternalAuthnInfo(userBuilder, i)); - IamAccount newAccount = userService.createAccount(userBuilder.build()); + IamAccount newAccount = + accountService.createAccount(userConverter.fromScim(userBuilder.build())); newAccount.setConfirmationKey(tokenGenerator.generateToken()); newAccount.setActive(false); @@ -145,7 +149,9 @@ public List listRequests(IamRegistrationRequestStatus st List result = new ArrayList<>(); if (status != null) { - result = requestRepository.findByStatus(status).get(); + result = requestRepository.findByStatus(status).orElseThrow( + () -> new IllegalStateException("No request found with status: " + status.name())); + } else { Sort srt = new Sort(Sort.Direction.ASC, "creationTime"); Iterable iter = requestRepository.findAll(srt); @@ -167,7 +173,7 @@ public List listRequests(IamRegistrationRequestStatus st @Override public List listPendingRequests() { - List result = requestRepository.findPendingRequests().get(); + List result = requestRepository.findPendingRequests(); List requests = new ArrayList<>(); @@ -275,7 +281,7 @@ private RegistrationRequestDto handleReject(IamRegistrationRequest request) { notificationService.createRequestRejectedMessage(request); RegistrationRequestDto retval = converter.fromEntity(request); - userService.delete(request.getAccount().getUuid()); + accountService.deleteAccount(request.getAccount()); eventPublisher.publishEvent(new RegistrationRejectEvent(this, request, "Reject registration request for user " + request.getAccount().getUsername())); @@ -283,4 +289,19 @@ private RegistrationRequestDto handleReject(IamRegistrationRequest request) { return retval; } + private void notesSanityChecks(final String notes) { + + if (notes == null) { + throw new IllegalArgumentException("Notes field cannot be null"); + } + + if (notes.trim().isEmpty()) { + throw new IllegalArgumentException("Notes field cannot be the empty string"); + } + } + + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + this.eventPublisher = publisher; + } + } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/registration/RegistrationController.java b/iam-login-service/src/main/java/it/infn/mw/iam/registration/RegistrationController.java index 54912c730..1e324755b 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/registration/RegistrationController.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/registration/RegistrationController.java @@ -1,8 +1,12 @@ package it.infn.mw.iam.registration; +import static java.lang.String.format; + import java.util.List; import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.security.access.prepost.PreAuthorize; @@ -30,6 +34,7 @@ @Profile("registration") public class RegistrationController { + public static final Logger LOG = LoggerFactory.getLogger(RegistrationController.class); private RegistrationRequestService service; private Optional getExternalAuthenticationInfo() { @@ -43,7 +48,7 @@ private Optional getExternalAuthenticati if (authn instanceof AbstractExternalAuthenticationToken) { return Optional - .of(((AbstractExternalAuthenticationToken) authn).toExernalAuthenticationInfo()); + .of(((AbstractExternalAuthenticationToken) authn).toExernalAuthenticationRegistrationInfo()); } return Optional.empty(); @@ -62,8 +67,7 @@ public Boolean usernameAvailable(@PathVariable("username") String username) { @RequestMapping(value = "/registration/email-available/{email:.+}", method = RequestMethod.GET) public Boolean emailAvailable(@PathVariable("email") String email) { - Boolean emailAvaiable = service.emailAvailable(email); - return emailAvaiable; + return service.emailAvailable(email); } @PreAuthorize("#oauth2.hasScope('registration:read') or hasRole('ADMIN')") @@ -101,7 +105,7 @@ public RegistrationRequestDto changeStatus(@PathVariable("uuid") String uuid, try { status = IamRegistrationRequestStatus.valueOf(decision); } catch (Exception e) { - throw new IllegalArgumentException(String.format("Operation [%s] not found", decision)); + throw new IllegalArgumentException(format("Operation [%s] not found", decision),e); } return service.updateStatus(uuid, status); @@ -120,6 +124,7 @@ public ModelAndView verify(final Model model, @PathVariable("token") String toke model.addAttribute("verificationSuccess", true); SecurityContextHolder.clearContext(); } catch (ScimResourceNotFoundException e) { + LOG.warn(e.getMessage(),e); String message = "Activation failed: " + e.getMessage(); model.addAttribute("verificationMessage", message); model.addAttribute("verificationFailure", true); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/registration/TokenGenerator.java b/iam-login-service/src/main/java/it/infn/mw/iam/registration/TokenGenerator.java index 634293c9e..3b9d1c75e 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/registration/TokenGenerator.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/registration/TokenGenerator.java @@ -1,5 +1,5 @@ package it.infn.mw.iam.registration; - +@FunctionalInterface public interface TokenGenerator { String generateToken(); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/util/ssh/RSAPublicKeyUtils.java b/iam-login-service/src/main/java/it/infn/mw/iam/util/ssh/RSAPublicKeyUtils.java index 04bb3850d..e2f04213f 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/util/ssh/RSAPublicKeyUtils.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/util/ssh/RSAPublicKeyUtils.java @@ -1,13 +1,15 @@ package it.infn.mw.iam.util.ssh; -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.codec.digest.MessageDigestAlgorithms; - import java.security.MessageDigest; import java.util.Base64; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.MessageDigestAlgorithms; + public class RSAPublicKeyUtils { + private RSAPublicKeyUtils() {} + public static String getMD5Fingerprint(String key) { return buildMD5Fingerprint(key); @@ -33,7 +35,7 @@ private static String buildMD5Fingerprint(String key) throws InvalidSshKeyExcept byte[] digest = MessageDigest.getInstance(MessageDigestAlgorithms.MD5).digest(decodedKey); fingerprint = Hex.encodeHexString(digest); - } catch (Throwable e) { + } catch (Exception e) { throw new InvalidSshKeyException( "Error during fingerprint generation: RSA key is not base64 encoded", e); @@ -52,7 +54,7 @@ private static String buildSHA256Fingerprint(String key) throws InvalidSshKeyExc byte[] digest = MessageDigest.getInstance(MessageDigestAlgorithms.SHA_256).digest(decodedKey); fingerprint = Base64.getEncoder().encodeToString(digest); - } catch (Throwable e) { + } catch (Exception e) { throw new InvalidSshKeyException( "Error during fingerprint generation: RSA key is not base64 encoded", e); diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/util/x509/X509Utils.java b/iam-login-service/src/main/java/it/infn/mw/iam/util/x509/X509Utils.java index ef918cc07..9a18112dd 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/util/x509/X509Utils.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/util/x509/X509Utils.java @@ -24,7 +24,7 @@ public static X509Certificate getX509CertificateFromString(String certValue) } catch (IllegalArgumentException iae) { throw new ScimValidationException( - "Error in conversion from String to x509 certificate: Not valid Base64 scheme"); + "Error in conversion from String to x509 certificate: Not valid Base64 scheme", iae); } X509Certificate cert = null; @@ -37,7 +37,7 @@ public static X509Certificate getX509CertificateFromString(String certValue) } catch (CertificateException ce) { throw new ScimValidationException( - "Error in conversion from String to x509 certificate: the base64 encoded string is not a valid certificate"); + "Error in conversion from String to x509 certificate: the base64 encoded string is not a valid certificate", ce); } return cert; diff --git a/iam-login-service/src/main/resources/application-h2-test.yml b/iam-login-service/src/main/resources/application-h2-test.yml index eb449586f..33337d7f3 100644 --- a/iam-login-service/src/main/resources/application-h2-test.yml +++ b/iam-login-service/src/main/resources/application-h2-test.yml @@ -1,6 +1,6 @@ spring: profiles: - include: google,saml + include: google,registration,saml datasource: driverClassName: org.h2.Driver url: jdbc:h2:mem:iam;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE diff --git a/iam-login-service/src/main/resources/application-saml.yml b/iam-login-service/src/main/resources/application-saml.yml index baf1d96f7..c660e2487 100644 --- a/iam-login-service/src/main/resources/application-saml.yml +++ b/iam-login-service/src/main/resources/application-saml.yml @@ -6,4 +6,13 @@ saml: key-password: ${IAM_SAML_KEY_PASSWORD:password} idp-metadata: ${IAM_SAML_IDP_METADATA:classpath:/saml/idp-metadata.xml} max-assertion-time-sec: ${IAM_SAML_MAX_ASSERTION_TIME:3000} - max-authentication-age-sec: ${IAM_SAML_MAX_AUTHENTICATION_AGE:86400} \ No newline at end of file + max-authentication-age-sec: ${IAM_SAML_MAX_AUTHENTICATION_AGE:86400} + id-resolvers: ${IAM_SAML_ID_RESOLVERS:eduPersonUniqueId,eduPersonTargetedId,eduPersonPrincipalName} + metadata-lookup-service-refresh-period-sec: ${IAM_SAML_METADATA_LOOKUP_SERVICE_REFRESH_PERIOD_SEC:600} + + jit-account-provisioning: + enabled: ${IAM_SAML_JIT_ACCOUNT_PROVISIONING_ENABLED:false} + trusted-idps: ${IAM_SAML_JIT_ACCOUNT_PROVISIONING_TRUSTED_IDPS:all} + cleanup-task-enabled: ${IAM_SAML_JIT_ACCOUNT_PROVISIONING_CLEANUP_TASK_ENABLED:false} + cleanup-task-period-sec: ${IAM_SAML_JIT_ACCOUNT_PROVISIONING_CLEANUP_TASK_PERIOD_SEC:86400} + inactive-account-lifetime-days: ${IAM_SAML_JIT_ACCOUNT_PROVISIONING_INACTIVE_ACCOUNT_LIFETIME_DAYS:15} \ No newline at end of file diff --git a/iam-login-service/src/main/resources/application.yml b/iam-login-service/src/main/resources/application.yml index 13cfbed13..e0fa3b3a7 100644 --- a/iam-login-service/src/main/resources/application.yml +++ b/iam-login-service/src/main/resources/application.yml @@ -13,7 +13,7 @@ server: spring: profiles: - active: h2-test,registration + active: h2-test application: name: INDIGO IAM @@ -92,3 +92,12 @@ task: endpoints: healthMail: path: /health/mail + externalService: + path: /health/external + +health: + timeout: 5000 + googleEndpoint: http://www.google.it + +accountLinking: + disable: ${IAM_ACCOUNT_LINKING_DISABLE:false} diff --git a/iam-login-service/src/main/resources/templates/resetPassword.vm b/iam-login-service/src/main/resources/templates/resetPassword.vm index 6dc60a5d9..9932b53a1 100644 --- a/iam-login-service/src/main/resources/templates/resetPassword.vm +++ b/iam-login-service/src/main/resources/templates/resetPassword.vm @@ -4,5 +4,8 @@ Follow this link to set a new password for your $organisationName account: $resetPasswordUrl +Once your password has been reset, you can login to the IAM using the following username: + +$username The $organisationName registration service diff --git a/iam-login-service/src/main/webapp/WEB-INF/tags/header.tag b/iam-login-service/src/main/webapp/WEB-INF/tags/header.tag index 16d1817e7..36ccabdd6 100644 --- a/iam-login-service/src/main/webapp/WEB-INF/tags/header.tag +++ b/iam-login-service/src/main/webapp/WEB-INF/tags/header.tag @@ -24,7 +24,7 @@ - + diff --git a/iam-login-service/src/main/webapp/WEB-INF/tags/iam/page.tag b/iam-login-service/src/main/webapp/WEB-INF/tags/iam/page.tag index a89981469..fa2e3cb80 100644 --- a/iam-login-service/src/main/webapp/WEB-INF/tags/iam/page.tag +++ b/iam-login-service/src/main/webapp/WEB-INF/tags/iam/page.tag @@ -46,7 +46,7 @@ + href="/webjars/font-awesome/css/font-awesome.css"> - \ No newline at end of file + diff --git a/iam-login-service/src/main/webapp/WEB-INF/tags/iamHeader.tag b/iam-login-service/src/main/webapp/WEB-INF/tags/iamHeader.tag index d44b52a04..a8b8537bc 100644 --- a/iam-login-service/src/main/webapp/WEB-INF/tags/iamHeader.tag +++ b/iam-login-service/src/main/webapp/WEB-INF/tags/iamHeader.tag @@ -32,9 +32,10 @@ rel="stylesheet" href="resources/iam/css/ionicons/ionicons.min.css"> + + href="/webjars/font-awesome/css/font-awesome.css"> + toaster-options="{'close-button': true, 'time-out':{ 'toast-error': 5000, 'toast-success': 5000, 'toast-warning': 5000 }, 'position-class': 'toast-top-center'}">
@@ -97,12 +97,12 @@ + - - + @@ -117,7 +117,8 @@ - + + diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/HELP-US-OUT.txt b/iam-login-service/src/main/webapp/resources/font-awesome/HELP-US-OUT.txt deleted file mode 100644 index 83d083dd7..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/HELP-US-OUT.txt +++ /dev/null @@ -1,7 +0,0 @@ -I hope you love Font Awesome. If you've found it useful, please do me a favor and check out my latest project, -Fort Awesome (https://fortawesome.com). It makes it easy to put the perfect icons on your website. Choose from our awesome, -comprehensive icon sets or copy and paste your own. - -Please. Check it out. - --Dave Gandy diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/css/font-awesome.css b/iam-login-service/src/main/webapp/resources/font-awesome/css/font-awesome.css deleted file mode 100644 index bb0fe51ad..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/css/font-awesome.css +++ /dev/null @@ -1,2178 +0,0 @@ -/*! - * Font Awesome 4.6.1 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */ -/* FONT PATH - * -------------------------- */ -@font-face { - font-family: 'FontAwesome'; - src: url('../fonts/fontawesome-webfont.eot?v=4.6.1'); - src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.6.1') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.6.1') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.6.1') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.6.1') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.6.1#fontawesomeregular') format('svg'); - font-weight: normal; - font-style: normal; -} -.fa { - display: inline-block; - font: normal normal normal 14px/1 FontAwesome; - font-size: inherit; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -/* makes the font 33% larger relative to the icon container */ -.fa-lg { - font-size: 1.33333333em; - line-height: 0.75em; - vertical-align: -15%; -} -.fa-2x { - font-size: 2em; -} -.fa-3x { - font-size: 3em; -} -.fa-4x { - font-size: 4em; -} -.fa-5x { - font-size: 5em; -} -.fa-fw { - width: 1.28571429em; - text-align: center; -} -.fa-ul { - padding-left: 0; - margin-left: 2.14285714em; - list-style-type: none; -} -.fa-ul > li { - position: relative; -} -.fa-li { - position: absolute; - left: -2.14285714em; - width: 2.14285714em; - top: 0.14285714em; - text-align: center; -} -.fa-li.fa-lg { - left: -1.85714286em; -} -.fa-border { - padding: .2em .25em .15em; - border: solid 0.08em #eeeeee; - border-radius: .1em; -} -.fa-pull-left { - float: left; -} -.fa-pull-right { - float: right; -} -.fa.fa-pull-left { - margin-right: .3em; -} -.fa.fa-pull-right { - margin-left: .3em; -} -/* Deprecated as of 4.4.0 */ -.pull-right { - float: right; -} -.pull-left { - float: left; -} -.fa.pull-left { - margin-right: .3em; -} -.fa.pull-right { - margin-left: .3em; -} -.fa-spin { - -webkit-animation: fa-spin 2s infinite linear; - animation: fa-spin 2s infinite linear; -} -.fa-pulse { - -webkit-animation: fa-spin 1s infinite steps(8); - animation: fa-spin 1s infinite steps(8); -} -@-webkit-keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} -@keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} -.fa-rotate-90 { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; - -webkit-transform: rotate(90deg); - -ms-transform: rotate(90deg); - transform: rotate(90deg); -} -.fa-rotate-180 { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; - -webkit-transform: rotate(180deg); - -ms-transform: rotate(180deg); - transform: rotate(180deg); -} -.fa-rotate-270 { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; - -webkit-transform: rotate(270deg); - -ms-transform: rotate(270deg); - transform: rotate(270deg); -} -.fa-flip-horizontal { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; - -webkit-transform: scale(-1, 1); - -ms-transform: scale(-1, 1); - transform: scale(-1, 1); -} -.fa-flip-vertical { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; - -webkit-transform: scale(1, -1); - -ms-transform: scale(1, -1); - transform: scale(1, -1); -} -:root .fa-rotate-90, -:root .fa-rotate-180, -:root .fa-rotate-270, -:root .fa-flip-horizontal, -:root .fa-flip-vertical { - filter: none; -} -.fa-stack { - position: relative; - display: inline-block; - width: 2em; - height: 2em; - line-height: 2em; - vertical-align: middle; -} -.fa-stack-1x, -.fa-stack-2x { - position: absolute; - left: 0; - width: 100%; - text-align: center; -} -.fa-stack-1x { - line-height: inherit; -} -.fa-stack-2x { - font-size: 2em; -} -.fa-inverse { - color: #ffffff; -} -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ -.fa-glass:before { - content: "\f000"; -} -.fa-music:before { - content: "\f001"; -} -.fa-search:before { - content: "\f002"; -} -.fa-envelope-o:before { - content: "\f003"; -} -.fa-heart:before { - content: "\f004"; -} -.fa-star:before { - content: "\f005"; -} -.fa-star-o:before { - content: "\f006"; -} -.fa-user:before { - content: "\f007"; -} -.fa-film:before { - content: "\f008"; -} -.fa-th-large:before { - content: "\f009"; -} -.fa-th:before { - content: "\f00a"; -} -.fa-th-list:before { - content: "\f00b"; -} -.fa-check:before { - content: "\f00c"; -} -.fa-remove:before, -.fa-close:before, -.fa-times:before { - content: "\f00d"; -} -.fa-search-plus:before { - content: "\f00e"; -} -.fa-search-minus:before { - content: "\f010"; -} -.fa-power-off:before { - content: "\f011"; -} -.fa-signal:before { - content: "\f012"; -} -.fa-gear:before, -.fa-cog:before { - content: "\f013"; -} -.fa-trash-o:before { - content: "\f014"; -} -.fa-home:before { - content: "\f015"; -} -.fa-file-o:before { - content: "\f016"; -} -.fa-clock-o:before { - content: "\f017"; -} -.fa-road:before { - content: "\f018"; -} -.fa-download:before { - content: "\f019"; -} -.fa-arrow-circle-o-down:before { - content: "\f01a"; -} -.fa-arrow-circle-o-up:before { - content: "\f01b"; -} -.fa-inbox:before { - content: "\f01c"; -} -.fa-play-circle-o:before { - content: "\f01d"; -} -.fa-rotate-right:before, -.fa-repeat:before { - content: "\f01e"; -} -.fa-refresh:before { - content: "\f021"; -} -.fa-list-alt:before { - content: "\f022"; -} -.fa-lock:before { - content: "\f023"; -} -.fa-flag:before { - content: "\f024"; -} -.fa-headphones:before { - content: "\f025"; -} -.fa-volume-off:before { - content: "\f026"; -} -.fa-volume-down:before { - content: "\f027"; -} -.fa-volume-up:before { - content: "\f028"; -} -.fa-qrcode:before { - content: "\f029"; -} -.fa-barcode:before { - content: "\f02a"; -} -.fa-tag:before { - content: "\f02b"; -} -.fa-tags:before { - content: "\f02c"; -} -.fa-book:before { - content: "\f02d"; -} -.fa-bookmark:before { - content: "\f02e"; -} -.fa-print:before { - content: "\f02f"; -} -.fa-camera:before { - content: "\f030"; -} -.fa-font:before { - content: "\f031"; -} -.fa-bold:before { - content: "\f032"; -} -.fa-italic:before { - content: "\f033"; -} -.fa-text-height:before { - content: "\f034"; -} -.fa-text-width:before { - content: "\f035"; -} -.fa-align-left:before { - content: "\f036"; -} -.fa-align-center:before { - content: "\f037"; -} -.fa-align-right:before { - content: "\f038"; -} -.fa-align-justify:before { - content: "\f039"; -} -.fa-list:before { - content: "\f03a"; -} -.fa-dedent:before, -.fa-outdent:before { - content: "\f03b"; -} -.fa-indent:before { - content: "\f03c"; -} -.fa-video-camera:before { - content: "\f03d"; -} -.fa-photo:before, -.fa-image:before, -.fa-picture-o:before { - content: "\f03e"; -} -.fa-pencil:before { - content: "\f040"; -} -.fa-map-marker:before { - content: "\f041"; -} -.fa-adjust:before { - content: "\f042"; -} -.fa-tint:before { - content: "\f043"; -} -.fa-edit:before, -.fa-pencil-square-o:before { - content: "\f044"; -} -.fa-share-square-o:before { - content: "\f045"; -} -.fa-check-square-o:before { - content: "\f046"; -} -.fa-arrows:before { - content: "\f047"; -} -.fa-step-backward:before { - content: "\f048"; -} -.fa-fast-backward:before { - content: "\f049"; -} -.fa-backward:before { - content: "\f04a"; -} -.fa-play:before { - content: "\f04b"; -} -.fa-pause:before { - content: "\f04c"; -} -.fa-stop:before { - content: "\f04d"; -} -.fa-forward:before { - content: "\f04e"; -} -.fa-fast-forward:before { - content: "\f050"; -} -.fa-step-forward:before { - content: "\f051"; -} -.fa-eject:before { - content: "\f052"; -} -.fa-chevron-left:before { - content: "\f053"; -} -.fa-chevron-right:before { - content: "\f054"; -} -.fa-plus-circle:before { - content: "\f055"; -} -.fa-minus-circle:before { - content: "\f056"; -} -.fa-times-circle:before { - content: "\f057"; -} -.fa-check-circle:before { - content: "\f058"; -} -.fa-question-circle:before { - content: "\f059"; -} -.fa-info-circle:before { - content: "\f05a"; -} -.fa-crosshairs:before { - content: "\f05b"; -} -.fa-times-circle-o:before { - content: "\f05c"; -} -.fa-check-circle-o:before { - content: "\f05d"; -} -.fa-ban:before { - content: "\f05e"; -} -.fa-arrow-left:before { - content: "\f060"; -} -.fa-arrow-right:before { - content: "\f061"; -} -.fa-arrow-up:before { - content: "\f062"; -} -.fa-arrow-down:before { - content: "\f063"; -} -.fa-mail-forward:before, -.fa-share:before { - content: "\f064"; -} -.fa-expand:before { - content: "\f065"; -} -.fa-compress:before { - content: "\f066"; -} -.fa-plus:before { - content: "\f067"; -} -.fa-minus:before { - content: "\f068"; -} -.fa-asterisk:before { - content: "\f069"; -} -.fa-exclamation-circle:before { - content: "\f06a"; -} -.fa-gift:before { - content: "\f06b"; -} -.fa-leaf:before { - content: "\f06c"; -} -.fa-fire:before { - content: "\f06d"; -} -.fa-eye:before { - content: "\f06e"; -} -.fa-eye-slash:before { - content: "\f070"; -} -.fa-warning:before, -.fa-exclamation-triangle:before { - content: "\f071"; -} -.fa-plane:before { - content: "\f072"; -} -.fa-calendar:before { - content: "\f073"; -} -.fa-random:before { - content: "\f074"; -} -.fa-comment:before { - content: "\f075"; -} -.fa-magnet:before { - content: "\f076"; -} -.fa-chevron-up:before { - content: "\f077"; -} -.fa-chevron-down:before { - content: "\f078"; -} -.fa-retweet:before { - content: "\f079"; -} -.fa-shopping-cart:before { - content: "\f07a"; -} -.fa-folder:before { - content: "\f07b"; -} -.fa-folder-open:before { - content: "\f07c"; -} -.fa-arrows-v:before { - content: "\f07d"; -} -.fa-arrows-h:before { - content: "\f07e"; -} -.fa-bar-chart-o:before, -.fa-bar-chart:before { - content: "\f080"; -} -.fa-twitter-square:before { - content: "\f081"; -} -.fa-facebook-square:before { - content: "\f082"; -} -.fa-camera-retro:before { - content: "\f083"; -} -.fa-key:before { - content: "\f084"; -} -.fa-gears:before, -.fa-cogs:before { - content: "\f085"; -} -.fa-comments:before { - content: "\f086"; -} -.fa-thumbs-o-up:before { - content: "\f087"; -} -.fa-thumbs-o-down:before { - content: "\f088"; -} -.fa-star-half:before { - content: "\f089"; -} -.fa-heart-o:before { - content: "\f08a"; -} -.fa-sign-out:before { - content: "\f08b"; -} -.fa-linkedin-square:before { - content: "\f08c"; -} -.fa-thumb-tack:before { - content: "\f08d"; -} -.fa-external-link:before { - content: "\f08e"; -} -.fa-sign-in:before { - content: "\f090"; -} -.fa-trophy:before { - content: "\f091"; -} -.fa-github-square:before { - content: "\f092"; -} -.fa-upload:before { - content: "\f093"; -} -.fa-lemon-o:before { - content: "\f094"; -} -.fa-phone:before { - content: "\f095"; -} -.fa-square-o:before { - content: "\f096"; -} -.fa-bookmark-o:before { - content: "\f097"; -} -.fa-phone-square:before { - content: "\f098"; -} -.fa-twitter:before { - content: "\f099"; -} -.fa-facebook-f:before, -.fa-facebook:before { - content: "\f09a"; -} -.fa-github:before { - content: "\f09b"; -} -.fa-unlock:before { - content: "\f09c"; -} -.fa-credit-card:before { - content: "\f09d"; -} -.fa-feed:before, -.fa-rss:before { - content: "\f09e"; -} -.fa-hdd-o:before { - content: "\f0a0"; -} -.fa-bullhorn:before { - content: "\f0a1"; -} -.fa-bell:before { - content: "\f0f3"; -} -.fa-certificate:before { - content: "\f0a3"; -} -.fa-hand-o-right:before { - content: "\f0a4"; -} -.fa-hand-o-left:before { - content: "\f0a5"; -} -.fa-hand-o-up:before { - content: "\f0a6"; -} -.fa-hand-o-down:before { - content: "\f0a7"; -} -.fa-arrow-circle-left:before { - content: "\f0a8"; -} -.fa-arrow-circle-right:before { - content: "\f0a9"; -} -.fa-arrow-circle-up:before { - content: "\f0aa"; -} -.fa-arrow-circle-down:before { - content: "\f0ab"; -} -.fa-globe:before { - content: "\f0ac"; -} -.fa-wrench:before { - content: "\f0ad"; -} -.fa-tasks:before { - content: "\f0ae"; -} -.fa-filter:before { - content: "\f0b0"; -} -.fa-briefcase:before { - content: "\f0b1"; -} -.fa-arrows-alt:before { - content: "\f0b2"; -} -.fa-group:before, -.fa-users:before { - content: "\f0c0"; -} -.fa-chain:before, -.fa-link:before { - content: "\f0c1"; -} -.fa-cloud:before { - content: "\f0c2"; -} -.fa-flask:before { - content: "\f0c3"; -} -.fa-cut:before, -.fa-scissors:before { - content: "\f0c4"; -} -.fa-copy:before, -.fa-files-o:before { - content: "\f0c5"; -} -.fa-paperclip:before { - content: "\f0c6"; -} -.fa-save:before, -.fa-floppy-o:before { - content: "\f0c7"; -} -.fa-square:before { - content: "\f0c8"; -} -.fa-navicon:before, -.fa-reorder:before, -.fa-bars:before { - content: "\f0c9"; -} -.fa-list-ul:before { - content: "\f0ca"; -} -.fa-list-ol:before { - content: "\f0cb"; -} -.fa-strikethrough:before { - content: "\f0cc"; -} -.fa-underline:before { - content: "\f0cd"; -} -.fa-table:before { - content: "\f0ce"; -} -.fa-magic:before { - content: "\f0d0"; -} -.fa-truck:before { - content: "\f0d1"; -} -.fa-pinterest:before { - content: "\f0d2"; -} -.fa-pinterest-square:before { - content: "\f0d3"; -} -.fa-google-plus-square:before { - content: "\f0d4"; -} -.fa-google-plus:before { - content: "\f0d5"; -} -.fa-money:before { - content: "\f0d6"; -} -.fa-caret-down:before { - content: "\f0d7"; -} -.fa-caret-up:before { - content: "\f0d8"; -} -.fa-caret-left:before { - content: "\f0d9"; -} -.fa-caret-right:before { - content: "\f0da"; -} -.fa-columns:before { - content: "\f0db"; -} -.fa-unsorted:before, -.fa-sort:before { - content: "\f0dc"; -} -.fa-sort-down:before, -.fa-sort-desc:before { - content: "\f0dd"; -} -.fa-sort-up:before, -.fa-sort-asc:before { - content: "\f0de"; -} -.fa-envelope:before { - content: "\f0e0"; -} -.fa-linkedin:before { - content: "\f0e1"; -} -.fa-rotate-left:before, -.fa-undo:before { - content: "\f0e2"; -} -.fa-legal:before, -.fa-gavel:before { - content: "\f0e3"; -} -.fa-dashboard:before, -.fa-tachometer:before { - content: "\f0e4"; -} -.fa-comment-o:before { - content: "\f0e5"; -} -.fa-comments-o:before { - content: "\f0e6"; -} -.fa-flash:before, -.fa-bolt:before { - content: "\f0e7"; -} -.fa-sitemap:before { - content: "\f0e8"; -} -.fa-umbrella:before { - content: "\f0e9"; -} -.fa-paste:before, -.fa-clipboard:before { - content: "\f0ea"; -} -.fa-lightbulb-o:before { - content: "\f0eb"; -} -.fa-exchange:before { - content: "\f0ec"; -} -.fa-cloud-download:before { - content: "\f0ed"; -} -.fa-cloud-upload:before { - content: "\f0ee"; -} -.fa-user-md:before { - content: "\f0f0"; -} -.fa-stethoscope:before { - content: "\f0f1"; -} -.fa-suitcase:before { - content: "\f0f2"; -} -.fa-bell-o:before { - content: "\f0a2"; -} -.fa-coffee:before { - content: "\f0f4"; -} -.fa-cutlery:before { - content: "\f0f5"; -} -.fa-file-text-o:before { - content: "\f0f6"; -} -.fa-building-o:before { - content: "\f0f7"; -} -.fa-hospital-o:before { - content: "\f0f8"; -} -.fa-ambulance:before { - content: "\f0f9"; -} -.fa-medkit:before { - content: "\f0fa"; -} -.fa-fighter-jet:before { - content: "\f0fb"; -} -.fa-beer:before { - content: "\f0fc"; -} -.fa-h-square:before { - content: "\f0fd"; -} -.fa-plus-square:before { - content: "\f0fe"; -} -.fa-angle-double-left:before { - content: "\f100"; -} -.fa-angle-double-right:before { - content: "\f101"; -} -.fa-angle-double-up:before { - content: "\f102"; -} -.fa-angle-double-down:before { - content: "\f103"; -} -.fa-angle-left:before { - content: "\f104"; -} -.fa-angle-right:before { - content: "\f105"; -} -.fa-angle-up:before { - content: "\f106"; -} -.fa-angle-down:before { - content: "\f107"; -} -.fa-desktop:before { - content: "\f108"; -} -.fa-laptop:before { - content: "\f109"; -} -.fa-tablet:before { - content: "\f10a"; -} -.fa-mobile-phone:before, -.fa-mobile:before { - content: "\f10b"; -} -.fa-circle-o:before { - content: "\f10c"; -} -.fa-quote-left:before { - content: "\f10d"; -} -.fa-quote-right:before { - content: "\f10e"; -} -.fa-spinner:before { - content: "\f110"; -} -.fa-circle:before { - content: "\f111"; -} -.fa-mail-reply:before, -.fa-reply:before { - content: "\f112"; -} -.fa-github-alt:before { - content: "\f113"; -} -.fa-folder-o:before { - content: "\f114"; -} -.fa-folder-open-o:before { - content: "\f115"; -} -.fa-smile-o:before { - content: "\f118"; -} -.fa-frown-o:before { - content: "\f119"; -} -.fa-meh-o:before { - content: "\f11a"; -} -.fa-gamepad:before { - content: "\f11b"; -} -.fa-keyboard-o:before { - content: "\f11c"; -} -.fa-flag-o:before { - content: "\f11d"; -} -.fa-flag-checkered:before { - content: "\f11e"; -} -.fa-terminal:before { - content: "\f120"; -} -.fa-code:before { - content: "\f121"; -} -.fa-mail-reply-all:before, -.fa-reply-all:before { - content: "\f122"; -} -.fa-star-half-empty:before, -.fa-star-half-full:before, -.fa-star-half-o:before { - content: "\f123"; -} -.fa-location-arrow:before { - content: "\f124"; -} -.fa-crop:before { - content: "\f125"; -} -.fa-code-fork:before { - content: "\f126"; -} -.fa-unlink:before, -.fa-chain-broken:before { - content: "\f127"; -} -.fa-question:before { - content: "\f128"; -} -.fa-info:before { - content: "\f129"; -} -.fa-exclamation:before { - content: "\f12a"; -} -.fa-superscript:before { - content: "\f12b"; -} -.fa-subscript:before { - content: "\f12c"; -} -.fa-eraser:before { - content: "\f12d"; -} -.fa-puzzle-piece:before { - content: "\f12e"; -} -.fa-microphone:before { - content: "\f130"; -} -.fa-microphone-slash:before { - content: "\f131"; -} -.fa-shield:before { - content: "\f132"; -} -.fa-calendar-o:before { - content: "\f133"; -} -.fa-fire-extinguisher:before { - content: "\f134"; -} -.fa-rocket:before { - content: "\f135"; -} -.fa-maxcdn:before { - content: "\f136"; -} -.fa-chevron-circle-left:before { - content: "\f137"; -} -.fa-chevron-circle-right:before { - content: "\f138"; -} -.fa-chevron-circle-up:before { - content: "\f139"; -} -.fa-chevron-circle-down:before { - content: "\f13a"; -} -.fa-html5:before { - content: "\f13b"; -} -.fa-css3:before { - content: "\f13c"; -} -.fa-anchor:before { - content: "\f13d"; -} -.fa-unlock-alt:before { - content: "\f13e"; -} -.fa-bullseye:before { - content: "\f140"; -} -.fa-ellipsis-h:before { - content: "\f141"; -} -.fa-ellipsis-v:before { - content: "\f142"; -} -.fa-rss-square:before { - content: "\f143"; -} -.fa-play-circle:before { - content: "\f144"; -} -.fa-ticket:before { - content: "\f145"; -} -.fa-minus-square:before { - content: "\f146"; -} -.fa-minus-square-o:before { - content: "\f147"; -} -.fa-level-up:before { - content: "\f148"; -} -.fa-level-down:before { - content: "\f149"; -} -.fa-check-square:before { - content: "\f14a"; -} -.fa-pencil-square:before { - content: "\f14b"; -} -.fa-external-link-square:before { - content: "\f14c"; -} -.fa-share-square:before { - content: "\f14d"; -} -.fa-compass:before { - content: "\f14e"; -} -.fa-toggle-down:before, -.fa-caret-square-o-down:before { - content: "\f150"; -} -.fa-toggle-up:before, -.fa-caret-square-o-up:before { - content: "\f151"; -} -.fa-toggle-right:before, -.fa-caret-square-o-right:before { - content: "\f152"; -} -.fa-euro:before, -.fa-eur:before { - content: "\f153"; -} -.fa-gbp:before { - content: "\f154"; -} -.fa-dollar:before, -.fa-usd:before { - content: "\f155"; -} -.fa-rupee:before, -.fa-inr:before { - content: "\f156"; -} -.fa-cny:before, -.fa-rmb:before, -.fa-yen:before, -.fa-jpy:before { - content: "\f157"; -} -.fa-ruble:before, -.fa-rouble:before, -.fa-rub:before { - content: "\f158"; -} -.fa-won:before, -.fa-krw:before { - content: "\f159"; -} -.fa-bitcoin:before, -.fa-btc:before { - content: "\f15a"; -} -.fa-file:before { - content: "\f15b"; -} -.fa-file-text:before { - content: "\f15c"; -} -.fa-sort-alpha-asc:before { - content: "\f15d"; -} -.fa-sort-alpha-desc:before { - content: "\f15e"; -} -.fa-sort-amount-asc:before { - content: "\f160"; -} -.fa-sort-amount-desc:before { - content: "\f161"; -} -.fa-sort-numeric-asc:before { - content: "\f162"; -} -.fa-sort-numeric-desc:before { - content: "\f163"; -} -.fa-thumbs-up:before { - content: "\f164"; -} -.fa-thumbs-down:before { - content: "\f165"; -} -.fa-youtube-square:before { - content: "\f166"; -} -.fa-youtube:before { - content: "\f167"; -} -.fa-xing:before { - content: "\f168"; -} -.fa-xing-square:before { - content: "\f169"; -} -.fa-youtube-play:before { - content: "\f16a"; -} -.fa-dropbox:before { - content: "\f16b"; -} -.fa-stack-overflow:before { - content: "\f16c"; -} -.fa-instagram:before { - content: "\f16d"; -} -.fa-flickr:before { - content: "\f16e"; -} -.fa-adn:before { - content: "\f170"; -} -.fa-bitbucket:before { - content: "\f171"; -} -.fa-bitbucket-square:before { - content: "\f172"; -} -.fa-tumblr:before { - content: "\f173"; -} -.fa-tumblr-square:before { - content: "\f174"; -} -.fa-long-arrow-down:before { - content: "\f175"; -} -.fa-long-arrow-up:before { - content: "\f176"; -} -.fa-long-arrow-left:before { - content: "\f177"; -} -.fa-long-arrow-right:before { - content: "\f178"; -} -.fa-apple:before { - content: "\f179"; -} -.fa-windows:before { - content: "\f17a"; -} -.fa-android:before { - content: "\f17b"; -} -.fa-linux:before { - content: "\f17c"; -} -.fa-dribbble:before { - content: "\f17d"; -} -.fa-skype:before { - content: "\f17e"; -} -.fa-foursquare:before { - content: "\f180"; -} -.fa-trello:before { - content: "\f181"; -} -.fa-female:before { - content: "\f182"; -} -.fa-male:before { - content: "\f183"; -} -.fa-gittip:before, -.fa-gratipay:before { - content: "\f184"; -} -.fa-sun-o:before { - content: "\f185"; -} -.fa-moon-o:before { - content: "\f186"; -} -.fa-archive:before { - content: "\f187"; -} -.fa-bug:before { - content: "\f188"; -} -.fa-vk:before { - content: "\f189"; -} -.fa-weibo:before { - content: "\f18a"; -} -.fa-renren:before { - content: "\f18b"; -} -.fa-pagelines:before { - content: "\f18c"; -} -.fa-stack-exchange:before { - content: "\f18d"; -} -.fa-arrow-circle-o-right:before { - content: "\f18e"; -} -.fa-arrow-circle-o-left:before { - content: "\f190"; -} -.fa-toggle-left:before, -.fa-caret-square-o-left:before { - content: "\f191"; -} -.fa-dot-circle-o:before { - content: "\f192"; -} -.fa-wheelchair:before { - content: "\f193"; -} -.fa-vimeo-square:before { - content: "\f194"; -} -.fa-turkish-lira:before, -.fa-try:before { - content: "\f195"; -} -.fa-plus-square-o:before { - content: "\f196"; -} -.fa-space-shuttle:before { - content: "\f197"; -} -.fa-slack:before { - content: "\f198"; -} -.fa-envelope-square:before { - content: "\f199"; -} -.fa-wordpress:before { - content: "\f19a"; -} -.fa-openid:before { - content: "\f19b"; -} -.fa-institution:before, -.fa-bank:before, -.fa-university:before { - content: "\f19c"; -} -.fa-mortar-board:before, -.fa-graduation-cap:before { - content: "\f19d"; -} -.fa-yahoo:before { - content: "\f19e"; -} -.fa-google:before { - content: "\f1a0"; -} -.fa-reddit:before { - content: "\f1a1"; -} -.fa-reddit-square:before { - content: "\f1a2"; -} -.fa-stumbleupon-circle:before { - content: "\f1a3"; -} -.fa-stumbleupon:before { - content: "\f1a4"; -} -.fa-delicious:before { - content: "\f1a5"; -} -.fa-digg:before { - content: "\f1a6"; -} -.fa-pied-piper:before { - content: "\f1a7"; -} -.fa-pied-piper-alt:before { - content: "\f1a8"; -} -.fa-drupal:before { - content: "\f1a9"; -} -.fa-joomla:before { - content: "\f1aa"; -} -.fa-language:before { - content: "\f1ab"; -} -.fa-fax:before { - content: "\f1ac"; -} -.fa-building:before { - content: "\f1ad"; -} -.fa-child:before { - content: "\f1ae"; -} -.fa-paw:before { - content: "\f1b0"; -} -.fa-spoon:before { - content: "\f1b1"; -} -.fa-cube:before { - content: "\f1b2"; -} -.fa-cubes:before { - content: "\f1b3"; -} -.fa-behance:before { - content: "\f1b4"; -} -.fa-behance-square:before { - content: "\f1b5"; -} -.fa-steam:before { - content: "\f1b6"; -} -.fa-steam-square:before { - content: "\f1b7"; -} -.fa-recycle:before { - content: "\f1b8"; -} -.fa-automobile:before, -.fa-car:before { - content: "\f1b9"; -} -.fa-cab:before, -.fa-taxi:before { - content: "\f1ba"; -} -.fa-tree:before { - content: "\f1bb"; -} -.fa-spotify:before { - content: "\f1bc"; -} -.fa-deviantart:before { - content: "\f1bd"; -} -.fa-soundcloud:before { - content: "\f1be"; -} -.fa-database:before { - content: "\f1c0"; -} -.fa-file-pdf-o:before { - content: "\f1c1"; -} -.fa-file-word-o:before { - content: "\f1c2"; -} -.fa-file-excel-o:before { - content: "\f1c3"; -} -.fa-file-powerpoint-o:before { - content: "\f1c4"; -} -.fa-file-photo-o:before, -.fa-file-picture-o:before, -.fa-file-image-o:before { - content: "\f1c5"; -} -.fa-file-zip-o:before, -.fa-file-archive-o:before { - content: "\f1c6"; -} -.fa-file-sound-o:before, -.fa-file-audio-o:before { - content: "\f1c7"; -} -.fa-file-movie-o:before, -.fa-file-video-o:before { - content: "\f1c8"; -} -.fa-file-code-o:before { - content: "\f1c9"; -} -.fa-vine:before { - content: "\f1ca"; -} -.fa-codepen:before { - content: "\f1cb"; -} -.fa-jsfiddle:before { - content: "\f1cc"; -} -.fa-life-bouy:before, -.fa-life-buoy:before, -.fa-life-saver:before, -.fa-support:before, -.fa-life-ring:before { - content: "\f1cd"; -} -.fa-circle-o-notch:before { - content: "\f1ce"; -} -.fa-ra:before, -.fa-rebel:before { - content: "\f1d0"; -} -.fa-ge:before, -.fa-empire:before { - content: "\f1d1"; -} -.fa-git-square:before { - content: "\f1d2"; -} -.fa-git:before { - content: "\f1d3"; -} -.fa-y-combinator-square:before, -.fa-yc-square:before, -.fa-hacker-news:before { - content: "\f1d4"; -} -.fa-tencent-weibo:before { - content: "\f1d5"; -} -.fa-qq:before { - content: "\f1d6"; -} -.fa-wechat:before, -.fa-weixin:before { - content: "\f1d7"; -} -.fa-send:before, -.fa-paper-plane:before { - content: "\f1d8"; -} -.fa-send-o:before, -.fa-paper-plane-o:before { - content: "\f1d9"; -} -.fa-history:before { - content: "\f1da"; -} -.fa-circle-thin:before { - content: "\f1db"; -} -.fa-header:before { - content: "\f1dc"; -} -.fa-paragraph:before { - content: "\f1dd"; -} -.fa-sliders:before { - content: "\f1de"; -} -.fa-share-alt:before { - content: "\f1e0"; -} -.fa-share-alt-square:before { - content: "\f1e1"; -} -.fa-bomb:before { - content: "\f1e2"; -} -.fa-soccer-ball-o:before, -.fa-futbol-o:before { - content: "\f1e3"; -} -.fa-tty:before { - content: "\f1e4"; -} -.fa-binoculars:before { - content: "\f1e5"; -} -.fa-plug:before { - content: "\f1e6"; -} -.fa-slideshare:before { - content: "\f1e7"; -} -.fa-twitch:before { - content: "\f1e8"; -} -.fa-yelp:before { - content: "\f1e9"; -} -.fa-newspaper-o:before { - content: "\f1ea"; -} -.fa-wifi:before { - content: "\f1eb"; -} -.fa-calculator:before { - content: "\f1ec"; -} -.fa-paypal:before { - content: "\f1ed"; -} -.fa-google-wallet:before { - content: "\f1ee"; -} -.fa-cc-visa:before { - content: "\f1f0"; -} -.fa-cc-mastercard:before { - content: "\f1f1"; -} -.fa-cc-discover:before { - content: "\f1f2"; -} -.fa-cc-amex:before { - content: "\f1f3"; -} -.fa-cc-paypal:before { - content: "\f1f4"; -} -.fa-cc-stripe:before { - content: "\f1f5"; -} -.fa-bell-slash:before { - content: "\f1f6"; -} -.fa-bell-slash-o:before { - content: "\f1f7"; -} -.fa-trash:before { - content: "\f1f8"; -} -.fa-copyright:before { - content: "\f1f9"; -} -.fa-at:before { - content: "\f1fa"; -} -.fa-eyedropper:before { - content: "\f1fb"; -} -.fa-paint-brush:before { - content: "\f1fc"; -} -.fa-birthday-cake:before { - content: "\f1fd"; -} -.fa-area-chart:before { - content: "\f1fe"; -} -.fa-pie-chart:before { - content: "\f200"; -} -.fa-line-chart:before { - content: "\f201"; -} -.fa-lastfm:before { - content: "\f202"; -} -.fa-lastfm-square:before { - content: "\f203"; -} -.fa-toggle-off:before { - content: "\f204"; -} -.fa-toggle-on:before { - content: "\f205"; -} -.fa-bicycle:before { - content: "\f206"; -} -.fa-bus:before { - content: "\f207"; -} -.fa-ioxhost:before { - content: "\f208"; -} -.fa-angellist:before { - content: "\f209"; -} -.fa-cc:before { - content: "\f20a"; -} -.fa-shekel:before, -.fa-sheqel:before, -.fa-ils:before { - content: "\f20b"; -} -.fa-meanpath:before { - content: "\f20c"; -} -.fa-buysellads:before { - content: "\f20d"; -} -.fa-connectdevelop:before { - content: "\f20e"; -} -.fa-dashcube:before { - content: "\f210"; -} -.fa-forumbee:before { - content: "\f211"; -} -.fa-leanpub:before { - content: "\f212"; -} -.fa-sellsy:before { - content: "\f213"; -} -.fa-shirtsinbulk:before { - content: "\f214"; -} -.fa-simplybuilt:before { - content: "\f215"; -} -.fa-skyatlas:before { - content: "\f216"; -} -.fa-cart-plus:before { - content: "\f217"; -} -.fa-cart-arrow-down:before { - content: "\f218"; -} -.fa-diamond:before { - content: "\f219"; -} -.fa-ship:before { - content: "\f21a"; -} -.fa-user-secret:before { - content: "\f21b"; -} -.fa-motorcycle:before { - content: "\f21c"; -} -.fa-street-view:before { - content: "\f21d"; -} -.fa-heartbeat:before { - content: "\f21e"; -} -.fa-venus:before { - content: "\f221"; -} -.fa-mars:before { - content: "\f222"; -} -.fa-mercury:before { - content: "\f223"; -} -.fa-intersex:before, -.fa-transgender:before { - content: "\f224"; -} -.fa-transgender-alt:before { - content: "\f225"; -} -.fa-venus-double:before { - content: "\f226"; -} -.fa-mars-double:before { - content: "\f227"; -} -.fa-venus-mars:before { - content: "\f228"; -} -.fa-mars-stroke:before { - content: "\f229"; -} -.fa-mars-stroke-v:before { - content: "\f22a"; -} -.fa-mars-stroke-h:before { - content: "\f22b"; -} -.fa-neuter:before { - content: "\f22c"; -} -.fa-genderless:before { - content: "\f22d"; -} -.fa-facebook-official:before { - content: "\f230"; -} -.fa-pinterest-p:before { - content: "\f231"; -} -.fa-whatsapp:before { - content: "\f232"; -} -.fa-server:before { - content: "\f233"; -} -.fa-user-plus:before { - content: "\f234"; -} -.fa-user-times:before { - content: "\f235"; -} -.fa-hotel:before, -.fa-bed:before { - content: "\f236"; -} -.fa-viacoin:before { - content: "\f237"; -} -.fa-train:before { - content: "\f238"; -} -.fa-subway:before { - content: "\f239"; -} -.fa-medium:before { - content: "\f23a"; -} -.fa-yc:before, -.fa-y-combinator:before { - content: "\f23b"; -} -.fa-optin-monster:before { - content: "\f23c"; -} -.fa-opencart:before { - content: "\f23d"; -} -.fa-expeditedssl:before { - content: "\f23e"; -} -.fa-battery-4:before, -.fa-battery-full:before { - content: "\f240"; -} -.fa-battery-3:before, -.fa-battery-three-quarters:before { - content: "\f241"; -} -.fa-battery-2:before, -.fa-battery-half:before { - content: "\f242"; -} -.fa-battery-1:before, -.fa-battery-quarter:before { - content: "\f243"; -} -.fa-battery-0:before, -.fa-battery-empty:before { - content: "\f244"; -} -.fa-mouse-pointer:before { - content: "\f245"; -} -.fa-i-cursor:before { - content: "\f246"; -} -.fa-object-group:before { - content: "\f247"; -} -.fa-object-ungroup:before { - content: "\f248"; -} -.fa-sticky-note:before { - content: "\f249"; -} -.fa-sticky-note-o:before { - content: "\f24a"; -} -.fa-cc-jcb:before { - content: "\f24b"; -} -.fa-cc-diners-club:before { - content: "\f24c"; -} -.fa-clone:before { - content: "\f24d"; -} -.fa-balance-scale:before { - content: "\f24e"; -} -.fa-hourglass-o:before { - content: "\f250"; -} -.fa-hourglass-1:before, -.fa-hourglass-start:before { - content: "\f251"; -} -.fa-hourglass-2:before, -.fa-hourglass-half:before { - content: "\f252"; -} -.fa-hourglass-3:before, -.fa-hourglass-end:before { - content: "\f253"; -} -.fa-hourglass:before { - content: "\f254"; -} -.fa-hand-grab-o:before, -.fa-hand-rock-o:before { - content: "\f255"; -} -.fa-hand-stop-o:before, -.fa-hand-paper-o:before { - content: "\f256"; -} -.fa-hand-scissors-o:before { - content: "\f257"; -} -.fa-hand-lizard-o:before { - content: "\f258"; -} -.fa-hand-spock-o:before { - content: "\f259"; -} -.fa-hand-pointer-o:before { - content: "\f25a"; -} -.fa-hand-peace-o:before { - content: "\f25b"; -} -.fa-trademark:before { - content: "\f25c"; -} -.fa-registered:before { - content: "\f25d"; -} -.fa-creative-commons:before { - content: "\f25e"; -} -.fa-gg:before { - content: "\f260"; -} -.fa-gg-circle:before { - content: "\f261"; -} -.fa-tripadvisor:before { - content: "\f262"; -} -.fa-odnoklassniki:before { - content: "\f263"; -} -.fa-odnoklassniki-square:before { - content: "\f264"; -} -.fa-get-pocket:before { - content: "\f265"; -} -.fa-wikipedia-w:before { - content: "\f266"; -} -.fa-safari:before { - content: "\f267"; -} -.fa-chrome:before { - content: "\f268"; -} -.fa-firefox:before { - content: "\f269"; -} -.fa-opera:before { - content: "\f26a"; -} -.fa-internet-explorer:before { - content: "\f26b"; -} -.fa-tv:before, -.fa-television:before { - content: "\f26c"; -} -.fa-contao:before { - content: "\f26d"; -} -.fa-500px:before { - content: "\f26e"; -} -.fa-amazon:before { - content: "\f270"; -} -.fa-calendar-plus-o:before { - content: "\f271"; -} -.fa-calendar-minus-o:before { - content: "\f272"; -} -.fa-calendar-times-o:before { - content: "\f273"; -} -.fa-calendar-check-o:before { - content: "\f274"; -} -.fa-industry:before { - content: "\f275"; -} -.fa-map-pin:before { - content: "\f276"; -} -.fa-map-signs:before { - content: "\f277"; -} -.fa-map-o:before { - content: "\f278"; -} -.fa-map:before { - content: "\f279"; -} -.fa-commenting:before { - content: "\f27a"; -} -.fa-commenting-o:before { - content: "\f27b"; -} -.fa-houzz:before { - content: "\f27c"; -} -.fa-vimeo:before { - content: "\f27d"; -} -.fa-black-tie:before { - content: "\f27e"; -} -.fa-fonticons:before { - content: "\f280"; -} -.fa-reddit-alien:before { - content: "\f281"; -} -.fa-edge:before { - content: "\f282"; -} -.fa-credit-card-alt:before { - content: "\f283"; -} -.fa-codiepie:before { - content: "\f284"; -} -.fa-modx:before { - content: "\f285"; -} -.fa-fort-awesome:before { - content: "\f286"; -} -.fa-usb:before { - content: "\f287"; -} -.fa-product-hunt:before { - content: "\f288"; -} -.fa-mixcloud:before { - content: "\f289"; -} -.fa-scribd:before { - content: "\f28a"; -} -.fa-pause-circle:before { - content: "\f28b"; -} -.fa-pause-circle-o:before { - content: "\f28c"; -} -.fa-stop-circle:before { - content: "\f28d"; -} -.fa-stop-circle-o:before { - content: "\f28e"; -} -.fa-shopping-bag:before { - content: "\f290"; -} -.fa-shopping-basket:before { - content: "\f291"; -} -.fa-hashtag:before { - content: "\f292"; -} -.fa-bluetooth:before { - content: "\f293"; -} -.fa-bluetooth-b:before { - content: "\f294"; -} -.fa-percent:before { - content: "\f295"; -} -.fa-gitlab:before { - content: "\f296"; -} -.fa-wpbeginner:before { - content: "\f297"; -} -.fa-wpforms:before { - content: "\f298"; -} -.fa-envira:before { - content: "\f299"; -} -.fa-universal-access:before { - content: "\f29a"; -} -.fa-wheelchair-alt:before { - content: "\f29b"; -} -.fa-question-circle-o:before { - content: "\f29c"; -} -.fa-blind:before { - content: "\f29d"; -} -.fa-audio-description:before { - content: "\f29e"; -} -.fa-volume-control-phone:before { - content: "\f2a0"; -} -.fa-braille:before { - content: "\f2a1"; -} -.fa-assistive-listening-systems:before { - content: "\f2a2"; -} -.fa-asl-interpreting:before, -.fa-american-sign-language-interpreting:before { - content: "\f2a3"; -} -.fa-deafness:before, -.fa-hard-of-hearing:before, -.fa-deaf:before { - content: "\f2a4"; -} -.fa-glide:before { - content: "\f2a5"; -} -.fa-glide-g:before { - content: "\f2a6"; -} -.fa-signing:before, -.fa-sign-language:before { - content: "\f2a7"; -} -.fa-low-vision:before { - content: "\f2a8"; -} -.fa-viadeo:before { - content: "\f2a9"; -} -.fa-viadeo-square:before { - content: "\f2aa"; -} -.fa-snapchat:before { - content: "\f2ab"; -} -.fa-snapchat-ghost:before { - content: "\f2ac"; -} -.fa-snapchat-square:before { - content: "\f2ad"; -} -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - border: 0; -} -.sr-only-focusable:active, -.sr-only-focusable:focus { - position: static; - width: auto; - height: auto; - margin: 0; - overflow: visible; - clip: auto; -} diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/css/font-awesome.min.css b/iam-login-service/src/main/webapp/resources/font-awesome/css/font-awesome.min.css deleted file mode 100644 index 885b38403..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/css/font-awesome.min.css +++ /dev/null @@ -1,4 +0,0 @@ -/*! - * Font Awesome 4.6.1 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.6.1');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.6.1') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.6.1') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.6.1') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.6.1') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.6.1#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/fonts/FontAwesome.otf b/iam-login-service/src/main/webapp/resources/font-awesome/fonts/FontAwesome.otf deleted file mode 100644 index 59853bcda..000000000 Binary files a/iam-login-service/src/main/webapp/resources/font-awesome/fonts/FontAwesome.otf and /dev/null differ diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/fonts/fontawesome-webfont.eot b/iam-login-service/src/main/webapp/resources/font-awesome/fonts/fontawesome-webfont.eot deleted file mode 100644 index 96f92f9b8..000000000 Binary files a/iam-login-service/src/main/webapp/resources/font-awesome/fonts/fontawesome-webfont.eot and /dev/null differ diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/fonts/fontawesome-webfont.svg b/iam-login-service/src/main/webapp/resources/font-awesome/fonts/fontawesome-webfont.svg deleted file mode 100644 index 5a5f0ecd4..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/fonts/fontawesome-webfont.svg +++ /dev/null @@ -1,685 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/fonts/fontawesome-webfont.ttf b/iam-login-service/src/main/webapp/resources/font-awesome/fonts/fontawesome-webfont.ttf deleted file mode 100644 index 86784df96..000000000 Binary files a/iam-login-service/src/main/webapp/resources/font-awesome/fonts/fontawesome-webfont.ttf and /dev/null differ diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/fonts/fontawesome-webfont.woff b/iam-login-service/src/main/webapp/resources/font-awesome/fonts/fontawesome-webfont.woff deleted file mode 100644 index c7faa19c4..000000000 Binary files a/iam-login-service/src/main/webapp/resources/font-awesome/fonts/fontawesome-webfont.woff and /dev/null differ diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/fonts/fontawesome-webfont.woff2 b/iam-login-service/src/main/webapp/resources/font-awesome/fonts/fontawesome-webfont.woff2 deleted file mode 100644 index cab8571d5..000000000 Binary files a/iam-login-service/src/main/webapp/resources/font-awesome/fonts/fontawesome-webfont.woff2 and /dev/null differ diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/less/animated.less b/iam-login-service/src/main/webapp/resources/font-awesome/less/animated.less deleted file mode 100644 index 66ad52a5b..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/less/animated.less +++ /dev/null @@ -1,34 +0,0 @@ -// Animated Icons -// -------------------------- - -.@{fa-css-prefix}-spin { - -webkit-animation: fa-spin 2s infinite linear; - animation: fa-spin 2s infinite linear; -} - -.@{fa-css-prefix}-pulse { - -webkit-animation: fa-spin 1s infinite steps(8); - animation: fa-spin 1s infinite steps(8); -} - -@-webkit-keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} - -@keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/less/bordered-pulled.less b/iam-login-service/src/main/webapp/resources/font-awesome/less/bordered-pulled.less deleted file mode 100644 index f1c8ad75f..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/less/bordered-pulled.less +++ /dev/null @@ -1,25 +0,0 @@ -// Bordered & Pulled -// ------------------------- - -.@{fa-css-prefix}-border { - padding: .2em .25em .15em; - border: solid .08em @fa-border-color; - border-radius: .1em; -} - -.@{fa-css-prefix}-pull-left { float: left; } -.@{fa-css-prefix}-pull-right { float: right; } - -.@{fa-css-prefix} { - &.@{fa-css-prefix}-pull-left { margin-right: .3em; } - &.@{fa-css-prefix}-pull-right { margin-left: .3em; } -} - -/* Deprecated as of 4.4.0 */ -.pull-right { float: right; } -.pull-left { float: left; } - -.@{fa-css-prefix} { - &.pull-left { margin-right: .3em; } - &.pull-right { margin-left: .3em; } -} diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/less/core.less b/iam-login-service/src/main/webapp/resources/font-awesome/less/core.less deleted file mode 100644 index c577ac84a..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/less/core.less +++ /dev/null @@ -1,12 +0,0 @@ -// Base Class Definition -// ------------------------- - -.@{fa-css-prefix} { - display: inline-block; - font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration - font-size: inherit; // can't have font-size inherit on line above, so need to override - text-rendering: auto; // optimizelegibility throws things off #1094 - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -} diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/less/fixed-width.less b/iam-login-service/src/main/webapp/resources/font-awesome/less/fixed-width.less deleted file mode 100644 index 110289f2f..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/less/fixed-width.less +++ /dev/null @@ -1,6 +0,0 @@ -// Fixed Width Icons -// ------------------------- -.@{fa-css-prefix}-fw { - width: (18em / 14); - text-align: center; -} diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/less/font-awesome.less b/iam-login-service/src/main/webapp/resources/font-awesome/less/font-awesome.less deleted file mode 100644 index 767096008..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/less/font-awesome.less +++ /dev/null @@ -1,18 +0,0 @@ -/*! - * Font Awesome 4.6.1 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */ - -@import "variables.less"; -@import "mixins.less"; -@import "path.less"; -@import "core.less"; -@import "larger.less"; -@import "fixed-width.less"; -@import "list.less"; -@import "bordered-pulled.less"; -@import "animated.less"; -@import "rotated-flipped.less"; -@import "stacked.less"; -@import "icons.less"; -@import "screen-reader.less"; diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/less/icons.less b/iam-login-service/src/main/webapp/resources/font-awesome/less/icons.less deleted file mode 100644 index c5e643091..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/less/icons.less +++ /dev/null @@ -1,724 +0,0 @@ -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ - -.@{fa-css-prefix}-glass:before { content: @fa-var-glass; } -.@{fa-css-prefix}-music:before { content: @fa-var-music; } -.@{fa-css-prefix}-search:before { content: @fa-var-search; } -.@{fa-css-prefix}-envelope-o:before { content: @fa-var-envelope-o; } -.@{fa-css-prefix}-heart:before { content: @fa-var-heart; } -.@{fa-css-prefix}-star:before { content: @fa-var-star; } -.@{fa-css-prefix}-star-o:before { content: @fa-var-star-o; } -.@{fa-css-prefix}-user:before { content: @fa-var-user; } -.@{fa-css-prefix}-film:before { content: @fa-var-film; } -.@{fa-css-prefix}-th-large:before { content: @fa-var-th-large; } -.@{fa-css-prefix}-th:before { content: @fa-var-th; } -.@{fa-css-prefix}-th-list:before { content: @fa-var-th-list; } -.@{fa-css-prefix}-check:before { content: @fa-var-check; } -.@{fa-css-prefix}-remove:before, -.@{fa-css-prefix}-close:before, -.@{fa-css-prefix}-times:before { content: @fa-var-times; } -.@{fa-css-prefix}-search-plus:before { content: @fa-var-search-plus; } -.@{fa-css-prefix}-search-minus:before { content: @fa-var-search-minus; } -.@{fa-css-prefix}-power-off:before { content: @fa-var-power-off; } -.@{fa-css-prefix}-signal:before { content: @fa-var-signal; } -.@{fa-css-prefix}-gear:before, -.@{fa-css-prefix}-cog:before { content: @fa-var-cog; } -.@{fa-css-prefix}-trash-o:before { content: @fa-var-trash-o; } -.@{fa-css-prefix}-home:before { content: @fa-var-home; } -.@{fa-css-prefix}-file-o:before { content: @fa-var-file-o; } -.@{fa-css-prefix}-clock-o:before { content: @fa-var-clock-o; } -.@{fa-css-prefix}-road:before { content: @fa-var-road; } -.@{fa-css-prefix}-download:before { content: @fa-var-download; } -.@{fa-css-prefix}-arrow-circle-o-down:before { content: @fa-var-arrow-circle-o-down; } -.@{fa-css-prefix}-arrow-circle-o-up:before { content: @fa-var-arrow-circle-o-up; } -.@{fa-css-prefix}-inbox:before { content: @fa-var-inbox; } -.@{fa-css-prefix}-play-circle-o:before { content: @fa-var-play-circle-o; } -.@{fa-css-prefix}-rotate-right:before, -.@{fa-css-prefix}-repeat:before { content: @fa-var-repeat; } -.@{fa-css-prefix}-refresh:before { content: @fa-var-refresh; } -.@{fa-css-prefix}-list-alt:before { content: @fa-var-list-alt; } -.@{fa-css-prefix}-lock:before { content: @fa-var-lock; } -.@{fa-css-prefix}-flag:before { content: @fa-var-flag; } -.@{fa-css-prefix}-headphones:before { content: @fa-var-headphones; } -.@{fa-css-prefix}-volume-off:before { content: @fa-var-volume-off; } -.@{fa-css-prefix}-volume-down:before { content: @fa-var-volume-down; } -.@{fa-css-prefix}-volume-up:before { content: @fa-var-volume-up; } -.@{fa-css-prefix}-qrcode:before { content: @fa-var-qrcode; } -.@{fa-css-prefix}-barcode:before { content: @fa-var-barcode; } -.@{fa-css-prefix}-tag:before { content: @fa-var-tag; } -.@{fa-css-prefix}-tags:before { content: @fa-var-tags; } -.@{fa-css-prefix}-book:before { content: @fa-var-book; } -.@{fa-css-prefix}-bookmark:before { content: @fa-var-bookmark; } -.@{fa-css-prefix}-print:before { content: @fa-var-print; } -.@{fa-css-prefix}-camera:before { content: @fa-var-camera; } -.@{fa-css-prefix}-font:before { content: @fa-var-font; } -.@{fa-css-prefix}-bold:before { content: @fa-var-bold; } -.@{fa-css-prefix}-italic:before { content: @fa-var-italic; } -.@{fa-css-prefix}-text-height:before { content: @fa-var-text-height; } -.@{fa-css-prefix}-text-width:before { content: @fa-var-text-width; } -.@{fa-css-prefix}-align-left:before { content: @fa-var-align-left; } -.@{fa-css-prefix}-align-center:before { content: @fa-var-align-center; } -.@{fa-css-prefix}-align-right:before { content: @fa-var-align-right; } -.@{fa-css-prefix}-align-justify:before { content: @fa-var-align-justify; } -.@{fa-css-prefix}-list:before { content: @fa-var-list; } -.@{fa-css-prefix}-dedent:before, -.@{fa-css-prefix}-outdent:before { content: @fa-var-outdent; } -.@{fa-css-prefix}-indent:before { content: @fa-var-indent; } -.@{fa-css-prefix}-video-camera:before { content: @fa-var-video-camera; } -.@{fa-css-prefix}-photo:before, -.@{fa-css-prefix}-image:before, -.@{fa-css-prefix}-picture-o:before { content: @fa-var-picture-o; } -.@{fa-css-prefix}-pencil:before { content: @fa-var-pencil; } -.@{fa-css-prefix}-map-marker:before { content: @fa-var-map-marker; } -.@{fa-css-prefix}-adjust:before { content: @fa-var-adjust; } -.@{fa-css-prefix}-tint:before { content: @fa-var-tint; } -.@{fa-css-prefix}-edit:before, -.@{fa-css-prefix}-pencil-square-o:before { content: @fa-var-pencil-square-o; } -.@{fa-css-prefix}-share-square-o:before { content: @fa-var-share-square-o; } -.@{fa-css-prefix}-check-square-o:before { content: @fa-var-check-square-o; } -.@{fa-css-prefix}-arrows:before { content: @fa-var-arrows; } -.@{fa-css-prefix}-step-backward:before { content: @fa-var-step-backward; } -.@{fa-css-prefix}-fast-backward:before { content: @fa-var-fast-backward; } -.@{fa-css-prefix}-backward:before { content: @fa-var-backward; } -.@{fa-css-prefix}-play:before { content: @fa-var-play; } -.@{fa-css-prefix}-pause:before { content: @fa-var-pause; } -.@{fa-css-prefix}-stop:before { content: @fa-var-stop; } -.@{fa-css-prefix}-forward:before { content: @fa-var-forward; } -.@{fa-css-prefix}-fast-forward:before { content: @fa-var-fast-forward; } -.@{fa-css-prefix}-step-forward:before { content: @fa-var-step-forward; } -.@{fa-css-prefix}-eject:before { content: @fa-var-eject; } -.@{fa-css-prefix}-chevron-left:before { content: @fa-var-chevron-left; } -.@{fa-css-prefix}-chevron-right:before { content: @fa-var-chevron-right; } -.@{fa-css-prefix}-plus-circle:before { content: @fa-var-plus-circle; } -.@{fa-css-prefix}-minus-circle:before { content: @fa-var-minus-circle; } -.@{fa-css-prefix}-times-circle:before { content: @fa-var-times-circle; } -.@{fa-css-prefix}-check-circle:before { content: @fa-var-check-circle; } -.@{fa-css-prefix}-question-circle:before { content: @fa-var-question-circle; } -.@{fa-css-prefix}-info-circle:before { content: @fa-var-info-circle; } -.@{fa-css-prefix}-crosshairs:before { content: @fa-var-crosshairs; } -.@{fa-css-prefix}-times-circle-o:before { content: @fa-var-times-circle-o; } -.@{fa-css-prefix}-check-circle-o:before { content: @fa-var-check-circle-o; } -.@{fa-css-prefix}-ban:before { content: @fa-var-ban; } -.@{fa-css-prefix}-arrow-left:before { content: @fa-var-arrow-left; } -.@{fa-css-prefix}-arrow-right:before { content: @fa-var-arrow-right; } -.@{fa-css-prefix}-arrow-up:before { content: @fa-var-arrow-up; } -.@{fa-css-prefix}-arrow-down:before { content: @fa-var-arrow-down; } -.@{fa-css-prefix}-mail-forward:before, -.@{fa-css-prefix}-share:before { content: @fa-var-share; } -.@{fa-css-prefix}-expand:before { content: @fa-var-expand; } -.@{fa-css-prefix}-compress:before { content: @fa-var-compress; } -.@{fa-css-prefix}-plus:before { content: @fa-var-plus; } -.@{fa-css-prefix}-minus:before { content: @fa-var-minus; } -.@{fa-css-prefix}-asterisk:before { content: @fa-var-asterisk; } -.@{fa-css-prefix}-exclamation-circle:before { content: @fa-var-exclamation-circle; } -.@{fa-css-prefix}-gift:before { content: @fa-var-gift; } -.@{fa-css-prefix}-leaf:before { content: @fa-var-leaf; } -.@{fa-css-prefix}-fire:before { content: @fa-var-fire; } -.@{fa-css-prefix}-eye:before { content: @fa-var-eye; } -.@{fa-css-prefix}-eye-slash:before { content: @fa-var-eye-slash; } -.@{fa-css-prefix}-warning:before, -.@{fa-css-prefix}-exclamation-triangle:before { content: @fa-var-exclamation-triangle; } -.@{fa-css-prefix}-plane:before { content: @fa-var-plane; } -.@{fa-css-prefix}-calendar:before { content: @fa-var-calendar; } -.@{fa-css-prefix}-random:before { content: @fa-var-random; } -.@{fa-css-prefix}-comment:before { content: @fa-var-comment; } -.@{fa-css-prefix}-magnet:before { content: @fa-var-magnet; } -.@{fa-css-prefix}-chevron-up:before { content: @fa-var-chevron-up; } -.@{fa-css-prefix}-chevron-down:before { content: @fa-var-chevron-down; } -.@{fa-css-prefix}-retweet:before { content: @fa-var-retweet; } -.@{fa-css-prefix}-shopping-cart:before { content: @fa-var-shopping-cart; } -.@{fa-css-prefix}-folder:before { content: @fa-var-folder; } -.@{fa-css-prefix}-folder-open:before { content: @fa-var-folder-open; } -.@{fa-css-prefix}-arrows-v:before { content: @fa-var-arrows-v; } -.@{fa-css-prefix}-arrows-h:before { content: @fa-var-arrows-h; } -.@{fa-css-prefix}-bar-chart-o:before, -.@{fa-css-prefix}-bar-chart:before { content: @fa-var-bar-chart; } -.@{fa-css-prefix}-twitter-square:before { content: @fa-var-twitter-square; } -.@{fa-css-prefix}-facebook-square:before { content: @fa-var-facebook-square; } -.@{fa-css-prefix}-camera-retro:before { content: @fa-var-camera-retro; } -.@{fa-css-prefix}-key:before { content: @fa-var-key; } -.@{fa-css-prefix}-gears:before, -.@{fa-css-prefix}-cogs:before { content: @fa-var-cogs; } -.@{fa-css-prefix}-comments:before { content: @fa-var-comments; } -.@{fa-css-prefix}-thumbs-o-up:before { content: @fa-var-thumbs-o-up; } -.@{fa-css-prefix}-thumbs-o-down:before { content: @fa-var-thumbs-o-down; } -.@{fa-css-prefix}-star-half:before { content: @fa-var-star-half; } -.@{fa-css-prefix}-heart-o:before { content: @fa-var-heart-o; } -.@{fa-css-prefix}-sign-out:before { content: @fa-var-sign-out; } -.@{fa-css-prefix}-linkedin-square:before { content: @fa-var-linkedin-square; } -.@{fa-css-prefix}-thumb-tack:before { content: @fa-var-thumb-tack; } -.@{fa-css-prefix}-external-link:before { content: @fa-var-external-link; } -.@{fa-css-prefix}-sign-in:before { content: @fa-var-sign-in; } -.@{fa-css-prefix}-trophy:before { content: @fa-var-trophy; } -.@{fa-css-prefix}-github-square:before { content: @fa-var-github-square; } -.@{fa-css-prefix}-upload:before { content: @fa-var-upload; } -.@{fa-css-prefix}-lemon-o:before { content: @fa-var-lemon-o; } -.@{fa-css-prefix}-phone:before { content: @fa-var-phone; } -.@{fa-css-prefix}-square-o:before { content: @fa-var-square-o; } -.@{fa-css-prefix}-bookmark-o:before { content: @fa-var-bookmark-o; } -.@{fa-css-prefix}-phone-square:before { content: @fa-var-phone-square; } -.@{fa-css-prefix}-twitter:before { content: @fa-var-twitter; } -.@{fa-css-prefix}-facebook-f:before, -.@{fa-css-prefix}-facebook:before { content: @fa-var-facebook; } -.@{fa-css-prefix}-github:before { content: @fa-var-github; } -.@{fa-css-prefix}-unlock:before { content: @fa-var-unlock; } -.@{fa-css-prefix}-credit-card:before { content: @fa-var-credit-card; } -.@{fa-css-prefix}-feed:before, -.@{fa-css-prefix}-rss:before { content: @fa-var-rss; } -.@{fa-css-prefix}-hdd-o:before { content: @fa-var-hdd-o; } -.@{fa-css-prefix}-bullhorn:before { content: @fa-var-bullhorn; } -.@{fa-css-prefix}-bell:before { content: @fa-var-bell; } -.@{fa-css-prefix}-certificate:before { content: @fa-var-certificate; } -.@{fa-css-prefix}-hand-o-right:before { content: @fa-var-hand-o-right; } -.@{fa-css-prefix}-hand-o-left:before { content: @fa-var-hand-o-left; } -.@{fa-css-prefix}-hand-o-up:before { content: @fa-var-hand-o-up; } -.@{fa-css-prefix}-hand-o-down:before { content: @fa-var-hand-o-down; } -.@{fa-css-prefix}-arrow-circle-left:before { content: @fa-var-arrow-circle-left; } -.@{fa-css-prefix}-arrow-circle-right:before { content: @fa-var-arrow-circle-right; } -.@{fa-css-prefix}-arrow-circle-up:before { content: @fa-var-arrow-circle-up; } -.@{fa-css-prefix}-arrow-circle-down:before { content: @fa-var-arrow-circle-down; } -.@{fa-css-prefix}-globe:before { content: @fa-var-globe; } -.@{fa-css-prefix}-wrench:before { content: @fa-var-wrench; } -.@{fa-css-prefix}-tasks:before { content: @fa-var-tasks; } -.@{fa-css-prefix}-filter:before { content: @fa-var-filter; } -.@{fa-css-prefix}-briefcase:before { content: @fa-var-briefcase; } -.@{fa-css-prefix}-arrows-alt:before { content: @fa-var-arrows-alt; } -.@{fa-css-prefix}-group:before, -.@{fa-css-prefix}-users:before { content: @fa-var-users; } -.@{fa-css-prefix}-chain:before, -.@{fa-css-prefix}-link:before { content: @fa-var-link; } -.@{fa-css-prefix}-cloud:before { content: @fa-var-cloud; } -.@{fa-css-prefix}-flask:before { content: @fa-var-flask; } -.@{fa-css-prefix}-cut:before, -.@{fa-css-prefix}-scissors:before { content: @fa-var-scissors; } -.@{fa-css-prefix}-copy:before, -.@{fa-css-prefix}-files-o:before { content: @fa-var-files-o; } -.@{fa-css-prefix}-paperclip:before { content: @fa-var-paperclip; } -.@{fa-css-prefix}-save:before, -.@{fa-css-prefix}-floppy-o:before { content: @fa-var-floppy-o; } -.@{fa-css-prefix}-square:before { content: @fa-var-square; } -.@{fa-css-prefix}-navicon:before, -.@{fa-css-prefix}-reorder:before, -.@{fa-css-prefix}-bars:before { content: @fa-var-bars; } -.@{fa-css-prefix}-list-ul:before { content: @fa-var-list-ul; } -.@{fa-css-prefix}-list-ol:before { content: @fa-var-list-ol; } -.@{fa-css-prefix}-strikethrough:before { content: @fa-var-strikethrough; } -.@{fa-css-prefix}-underline:before { content: @fa-var-underline; } -.@{fa-css-prefix}-table:before { content: @fa-var-table; } -.@{fa-css-prefix}-magic:before { content: @fa-var-magic; } -.@{fa-css-prefix}-truck:before { content: @fa-var-truck; } -.@{fa-css-prefix}-pinterest:before { content: @fa-var-pinterest; } -.@{fa-css-prefix}-pinterest-square:before { content: @fa-var-pinterest-square; } -.@{fa-css-prefix}-google-plus-square:before { content: @fa-var-google-plus-square; } -.@{fa-css-prefix}-google-plus:before { content: @fa-var-google-plus; } -.@{fa-css-prefix}-money:before { content: @fa-var-money; } -.@{fa-css-prefix}-caret-down:before { content: @fa-var-caret-down; } -.@{fa-css-prefix}-caret-up:before { content: @fa-var-caret-up; } -.@{fa-css-prefix}-caret-left:before { content: @fa-var-caret-left; } -.@{fa-css-prefix}-caret-right:before { content: @fa-var-caret-right; } -.@{fa-css-prefix}-columns:before { content: @fa-var-columns; } -.@{fa-css-prefix}-unsorted:before, -.@{fa-css-prefix}-sort:before { content: @fa-var-sort; } -.@{fa-css-prefix}-sort-down:before, -.@{fa-css-prefix}-sort-desc:before { content: @fa-var-sort-desc; } -.@{fa-css-prefix}-sort-up:before, -.@{fa-css-prefix}-sort-asc:before { content: @fa-var-sort-asc; } -.@{fa-css-prefix}-envelope:before { content: @fa-var-envelope; } -.@{fa-css-prefix}-linkedin:before { content: @fa-var-linkedin; } -.@{fa-css-prefix}-rotate-left:before, -.@{fa-css-prefix}-undo:before { content: @fa-var-undo; } -.@{fa-css-prefix}-legal:before, -.@{fa-css-prefix}-gavel:before { content: @fa-var-gavel; } -.@{fa-css-prefix}-dashboard:before, -.@{fa-css-prefix}-tachometer:before { content: @fa-var-tachometer; } -.@{fa-css-prefix}-comment-o:before { content: @fa-var-comment-o; } -.@{fa-css-prefix}-comments-o:before { content: @fa-var-comments-o; } -.@{fa-css-prefix}-flash:before, -.@{fa-css-prefix}-bolt:before { content: @fa-var-bolt; } -.@{fa-css-prefix}-sitemap:before { content: @fa-var-sitemap; } -.@{fa-css-prefix}-umbrella:before { content: @fa-var-umbrella; } -.@{fa-css-prefix}-paste:before, -.@{fa-css-prefix}-clipboard:before { content: @fa-var-clipboard; } -.@{fa-css-prefix}-lightbulb-o:before { content: @fa-var-lightbulb-o; } -.@{fa-css-prefix}-exchange:before { content: @fa-var-exchange; } -.@{fa-css-prefix}-cloud-download:before { content: @fa-var-cloud-download; } -.@{fa-css-prefix}-cloud-upload:before { content: @fa-var-cloud-upload; } -.@{fa-css-prefix}-user-md:before { content: @fa-var-user-md; } -.@{fa-css-prefix}-stethoscope:before { content: @fa-var-stethoscope; } -.@{fa-css-prefix}-suitcase:before { content: @fa-var-suitcase; } -.@{fa-css-prefix}-bell-o:before { content: @fa-var-bell-o; } -.@{fa-css-prefix}-coffee:before { content: @fa-var-coffee; } -.@{fa-css-prefix}-cutlery:before { content: @fa-var-cutlery; } -.@{fa-css-prefix}-file-text-o:before { content: @fa-var-file-text-o; } -.@{fa-css-prefix}-building-o:before { content: @fa-var-building-o; } -.@{fa-css-prefix}-hospital-o:before { content: @fa-var-hospital-o; } -.@{fa-css-prefix}-ambulance:before { content: @fa-var-ambulance; } -.@{fa-css-prefix}-medkit:before { content: @fa-var-medkit; } -.@{fa-css-prefix}-fighter-jet:before { content: @fa-var-fighter-jet; } -.@{fa-css-prefix}-beer:before { content: @fa-var-beer; } -.@{fa-css-prefix}-h-square:before { content: @fa-var-h-square; } -.@{fa-css-prefix}-plus-square:before { content: @fa-var-plus-square; } -.@{fa-css-prefix}-angle-double-left:before { content: @fa-var-angle-double-left; } -.@{fa-css-prefix}-angle-double-right:before { content: @fa-var-angle-double-right; } -.@{fa-css-prefix}-angle-double-up:before { content: @fa-var-angle-double-up; } -.@{fa-css-prefix}-angle-double-down:before { content: @fa-var-angle-double-down; } -.@{fa-css-prefix}-angle-left:before { content: @fa-var-angle-left; } -.@{fa-css-prefix}-angle-right:before { content: @fa-var-angle-right; } -.@{fa-css-prefix}-angle-up:before { content: @fa-var-angle-up; } -.@{fa-css-prefix}-angle-down:before { content: @fa-var-angle-down; } -.@{fa-css-prefix}-desktop:before { content: @fa-var-desktop; } -.@{fa-css-prefix}-laptop:before { content: @fa-var-laptop; } -.@{fa-css-prefix}-tablet:before { content: @fa-var-tablet; } -.@{fa-css-prefix}-mobile-phone:before, -.@{fa-css-prefix}-mobile:before { content: @fa-var-mobile; } -.@{fa-css-prefix}-circle-o:before { content: @fa-var-circle-o; } -.@{fa-css-prefix}-quote-left:before { content: @fa-var-quote-left; } -.@{fa-css-prefix}-quote-right:before { content: @fa-var-quote-right; } -.@{fa-css-prefix}-spinner:before { content: @fa-var-spinner; } -.@{fa-css-prefix}-circle:before { content: @fa-var-circle; } -.@{fa-css-prefix}-mail-reply:before, -.@{fa-css-prefix}-reply:before { content: @fa-var-reply; } -.@{fa-css-prefix}-github-alt:before { content: @fa-var-github-alt; } -.@{fa-css-prefix}-folder-o:before { content: @fa-var-folder-o; } -.@{fa-css-prefix}-folder-open-o:before { content: @fa-var-folder-open-o; } -.@{fa-css-prefix}-smile-o:before { content: @fa-var-smile-o; } -.@{fa-css-prefix}-frown-o:before { content: @fa-var-frown-o; } -.@{fa-css-prefix}-meh-o:before { content: @fa-var-meh-o; } -.@{fa-css-prefix}-gamepad:before { content: @fa-var-gamepad; } -.@{fa-css-prefix}-keyboard-o:before { content: @fa-var-keyboard-o; } -.@{fa-css-prefix}-flag-o:before { content: @fa-var-flag-o; } -.@{fa-css-prefix}-flag-checkered:before { content: @fa-var-flag-checkered; } -.@{fa-css-prefix}-terminal:before { content: @fa-var-terminal; } -.@{fa-css-prefix}-code:before { content: @fa-var-code; } -.@{fa-css-prefix}-mail-reply-all:before, -.@{fa-css-prefix}-reply-all:before { content: @fa-var-reply-all; } -.@{fa-css-prefix}-star-half-empty:before, -.@{fa-css-prefix}-star-half-full:before, -.@{fa-css-prefix}-star-half-o:before { content: @fa-var-star-half-o; } -.@{fa-css-prefix}-location-arrow:before { content: @fa-var-location-arrow; } -.@{fa-css-prefix}-crop:before { content: @fa-var-crop; } -.@{fa-css-prefix}-code-fork:before { content: @fa-var-code-fork; } -.@{fa-css-prefix}-unlink:before, -.@{fa-css-prefix}-chain-broken:before { content: @fa-var-chain-broken; } -.@{fa-css-prefix}-question:before { content: @fa-var-question; } -.@{fa-css-prefix}-info:before { content: @fa-var-info; } -.@{fa-css-prefix}-exclamation:before { content: @fa-var-exclamation; } -.@{fa-css-prefix}-superscript:before { content: @fa-var-superscript; } -.@{fa-css-prefix}-subscript:before { content: @fa-var-subscript; } -.@{fa-css-prefix}-eraser:before { content: @fa-var-eraser; } -.@{fa-css-prefix}-puzzle-piece:before { content: @fa-var-puzzle-piece; } -.@{fa-css-prefix}-microphone:before { content: @fa-var-microphone; } -.@{fa-css-prefix}-microphone-slash:before { content: @fa-var-microphone-slash; } -.@{fa-css-prefix}-shield:before { content: @fa-var-shield; } -.@{fa-css-prefix}-calendar-o:before { content: @fa-var-calendar-o; } -.@{fa-css-prefix}-fire-extinguisher:before { content: @fa-var-fire-extinguisher; } -.@{fa-css-prefix}-rocket:before { content: @fa-var-rocket; } -.@{fa-css-prefix}-maxcdn:before { content: @fa-var-maxcdn; } -.@{fa-css-prefix}-chevron-circle-left:before { content: @fa-var-chevron-circle-left; } -.@{fa-css-prefix}-chevron-circle-right:before { content: @fa-var-chevron-circle-right; } -.@{fa-css-prefix}-chevron-circle-up:before { content: @fa-var-chevron-circle-up; } -.@{fa-css-prefix}-chevron-circle-down:before { content: @fa-var-chevron-circle-down; } -.@{fa-css-prefix}-html5:before { content: @fa-var-html5; } -.@{fa-css-prefix}-css3:before { content: @fa-var-css3; } -.@{fa-css-prefix}-anchor:before { content: @fa-var-anchor; } -.@{fa-css-prefix}-unlock-alt:before { content: @fa-var-unlock-alt; } -.@{fa-css-prefix}-bullseye:before { content: @fa-var-bullseye; } -.@{fa-css-prefix}-ellipsis-h:before { content: @fa-var-ellipsis-h; } -.@{fa-css-prefix}-ellipsis-v:before { content: @fa-var-ellipsis-v; } -.@{fa-css-prefix}-rss-square:before { content: @fa-var-rss-square; } -.@{fa-css-prefix}-play-circle:before { content: @fa-var-play-circle; } -.@{fa-css-prefix}-ticket:before { content: @fa-var-ticket; } -.@{fa-css-prefix}-minus-square:before { content: @fa-var-minus-square; } -.@{fa-css-prefix}-minus-square-o:before { content: @fa-var-minus-square-o; } -.@{fa-css-prefix}-level-up:before { content: @fa-var-level-up; } -.@{fa-css-prefix}-level-down:before { content: @fa-var-level-down; } -.@{fa-css-prefix}-check-square:before { content: @fa-var-check-square; } -.@{fa-css-prefix}-pencil-square:before { content: @fa-var-pencil-square; } -.@{fa-css-prefix}-external-link-square:before { content: @fa-var-external-link-square; } -.@{fa-css-prefix}-share-square:before { content: @fa-var-share-square; } -.@{fa-css-prefix}-compass:before { content: @fa-var-compass; } -.@{fa-css-prefix}-toggle-down:before, -.@{fa-css-prefix}-caret-square-o-down:before { content: @fa-var-caret-square-o-down; } -.@{fa-css-prefix}-toggle-up:before, -.@{fa-css-prefix}-caret-square-o-up:before { content: @fa-var-caret-square-o-up; } -.@{fa-css-prefix}-toggle-right:before, -.@{fa-css-prefix}-caret-square-o-right:before { content: @fa-var-caret-square-o-right; } -.@{fa-css-prefix}-euro:before, -.@{fa-css-prefix}-eur:before { content: @fa-var-eur; } -.@{fa-css-prefix}-gbp:before { content: @fa-var-gbp; } -.@{fa-css-prefix}-dollar:before, -.@{fa-css-prefix}-usd:before { content: @fa-var-usd; } -.@{fa-css-prefix}-rupee:before, -.@{fa-css-prefix}-inr:before { content: @fa-var-inr; } -.@{fa-css-prefix}-cny:before, -.@{fa-css-prefix}-rmb:before, -.@{fa-css-prefix}-yen:before, -.@{fa-css-prefix}-jpy:before { content: @fa-var-jpy; } -.@{fa-css-prefix}-ruble:before, -.@{fa-css-prefix}-rouble:before, -.@{fa-css-prefix}-rub:before { content: @fa-var-rub; } -.@{fa-css-prefix}-won:before, -.@{fa-css-prefix}-krw:before { content: @fa-var-krw; } -.@{fa-css-prefix}-bitcoin:before, -.@{fa-css-prefix}-btc:before { content: @fa-var-btc; } -.@{fa-css-prefix}-file:before { content: @fa-var-file; } -.@{fa-css-prefix}-file-text:before { content: @fa-var-file-text; } -.@{fa-css-prefix}-sort-alpha-asc:before { content: @fa-var-sort-alpha-asc; } -.@{fa-css-prefix}-sort-alpha-desc:before { content: @fa-var-sort-alpha-desc; } -.@{fa-css-prefix}-sort-amount-asc:before { content: @fa-var-sort-amount-asc; } -.@{fa-css-prefix}-sort-amount-desc:before { content: @fa-var-sort-amount-desc; } -.@{fa-css-prefix}-sort-numeric-asc:before { content: @fa-var-sort-numeric-asc; } -.@{fa-css-prefix}-sort-numeric-desc:before { content: @fa-var-sort-numeric-desc; } -.@{fa-css-prefix}-thumbs-up:before { content: @fa-var-thumbs-up; } -.@{fa-css-prefix}-thumbs-down:before { content: @fa-var-thumbs-down; } -.@{fa-css-prefix}-youtube-square:before { content: @fa-var-youtube-square; } -.@{fa-css-prefix}-youtube:before { content: @fa-var-youtube; } -.@{fa-css-prefix}-xing:before { content: @fa-var-xing; } -.@{fa-css-prefix}-xing-square:before { content: @fa-var-xing-square; } -.@{fa-css-prefix}-youtube-play:before { content: @fa-var-youtube-play; } -.@{fa-css-prefix}-dropbox:before { content: @fa-var-dropbox; } -.@{fa-css-prefix}-stack-overflow:before { content: @fa-var-stack-overflow; } -.@{fa-css-prefix}-instagram:before { content: @fa-var-instagram; } -.@{fa-css-prefix}-flickr:before { content: @fa-var-flickr; } -.@{fa-css-prefix}-adn:before { content: @fa-var-adn; } -.@{fa-css-prefix}-bitbucket:before { content: @fa-var-bitbucket; } -.@{fa-css-prefix}-bitbucket-square:before { content: @fa-var-bitbucket-square; } -.@{fa-css-prefix}-tumblr:before { content: @fa-var-tumblr; } -.@{fa-css-prefix}-tumblr-square:before { content: @fa-var-tumblr-square; } -.@{fa-css-prefix}-long-arrow-down:before { content: @fa-var-long-arrow-down; } -.@{fa-css-prefix}-long-arrow-up:before { content: @fa-var-long-arrow-up; } -.@{fa-css-prefix}-long-arrow-left:before { content: @fa-var-long-arrow-left; } -.@{fa-css-prefix}-long-arrow-right:before { content: @fa-var-long-arrow-right; } -.@{fa-css-prefix}-apple:before { content: @fa-var-apple; } -.@{fa-css-prefix}-windows:before { content: @fa-var-windows; } -.@{fa-css-prefix}-android:before { content: @fa-var-android; } -.@{fa-css-prefix}-linux:before { content: @fa-var-linux; } -.@{fa-css-prefix}-dribbble:before { content: @fa-var-dribbble; } -.@{fa-css-prefix}-skype:before { content: @fa-var-skype; } -.@{fa-css-prefix}-foursquare:before { content: @fa-var-foursquare; } -.@{fa-css-prefix}-trello:before { content: @fa-var-trello; } -.@{fa-css-prefix}-female:before { content: @fa-var-female; } -.@{fa-css-prefix}-male:before { content: @fa-var-male; } -.@{fa-css-prefix}-gittip:before, -.@{fa-css-prefix}-gratipay:before { content: @fa-var-gratipay; } -.@{fa-css-prefix}-sun-o:before { content: @fa-var-sun-o; } -.@{fa-css-prefix}-moon-o:before { content: @fa-var-moon-o; } -.@{fa-css-prefix}-archive:before { content: @fa-var-archive; } -.@{fa-css-prefix}-bug:before { content: @fa-var-bug; } -.@{fa-css-prefix}-vk:before { content: @fa-var-vk; } -.@{fa-css-prefix}-weibo:before { content: @fa-var-weibo; } -.@{fa-css-prefix}-renren:before { content: @fa-var-renren; } -.@{fa-css-prefix}-pagelines:before { content: @fa-var-pagelines; } -.@{fa-css-prefix}-stack-exchange:before { content: @fa-var-stack-exchange; } -.@{fa-css-prefix}-arrow-circle-o-right:before { content: @fa-var-arrow-circle-o-right; } -.@{fa-css-prefix}-arrow-circle-o-left:before { content: @fa-var-arrow-circle-o-left; } -.@{fa-css-prefix}-toggle-left:before, -.@{fa-css-prefix}-caret-square-o-left:before { content: @fa-var-caret-square-o-left; } -.@{fa-css-prefix}-dot-circle-o:before { content: @fa-var-dot-circle-o; } -.@{fa-css-prefix}-wheelchair:before { content: @fa-var-wheelchair; } -.@{fa-css-prefix}-vimeo-square:before { content: @fa-var-vimeo-square; } -.@{fa-css-prefix}-turkish-lira:before, -.@{fa-css-prefix}-try:before { content: @fa-var-try; } -.@{fa-css-prefix}-plus-square-o:before { content: @fa-var-plus-square-o; } -.@{fa-css-prefix}-space-shuttle:before { content: @fa-var-space-shuttle; } -.@{fa-css-prefix}-slack:before { content: @fa-var-slack; } -.@{fa-css-prefix}-envelope-square:before { content: @fa-var-envelope-square; } -.@{fa-css-prefix}-wordpress:before { content: @fa-var-wordpress; } -.@{fa-css-prefix}-openid:before { content: @fa-var-openid; } -.@{fa-css-prefix}-institution:before, -.@{fa-css-prefix}-bank:before, -.@{fa-css-prefix}-university:before { content: @fa-var-university; } -.@{fa-css-prefix}-mortar-board:before, -.@{fa-css-prefix}-graduation-cap:before { content: @fa-var-graduation-cap; } -.@{fa-css-prefix}-yahoo:before { content: @fa-var-yahoo; } -.@{fa-css-prefix}-google:before { content: @fa-var-google; } -.@{fa-css-prefix}-reddit:before { content: @fa-var-reddit; } -.@{fa-css-prefix}-reddit-square:before { content: @fa-var-reddit-square; } -.@{fa-css-prefix}-stumbleupon-circle:before { content: @fa-var-stumbleupon-circle; } -.@{fa-css-prefix}-stumbleupon:before { content: @fa-var-stumbleupon; } -.@{fa-css-prefix}-delicious:before { content: @fa-var-delicious; } -.@{fa-css-prefix}-digg:before { content: @fa-var-digg; } -.@{fa-css-prefix}-pied-piper:before { content: @fa-var-pied-piper; } -.@{fa-css-prefix}-pied-piper-alt:before { content: @fa-var-pied-piper-alt; } -.@{fa-css-prefix}-drupal:before { content: @fa-var-drupal; } -.@{fa-css-prefix}-joomla:before { content: @fa-var-joomla; } -.@{fa-css-prefix}-language:before { content: @fa-var-language; } -.@{fa-css-prefix}-fax:before { content: @fa-var-fax; } -.@{fa-css-prefix}-building:before { content: @fa-var-building; } -.@{fa-css-prefix}-child:before { content: @fa-var-child; } -.@{fa-css-prefix}-paw:before { content: @fa-var-paw; } -.@{fa-css-prefix}-spoon:before { content: @fa-var-spoon; } -.@{fa-css-prefix}-cube:before { content: @fa-var-cube; } -.@{fa-css-prefix}-cubes:before { content: @fa-var-cubes; } -.@{fa-css-prefix}-behance:before { content: @fa-var-behance; } -.@{fa-css-prefix}-behance-square:before { content: @fa-var-behance-square; } -.@{fa-css-prefix}-steam:before { content: @fa-var-steam; } -.@{fa-css-prefix}-steam-square:before { content: @fa-var-steam-square; } -.@{fa-css-prefix}-recycle:before { content: @fa-var-recycle; } -.@{fa-css-prefix}-automobile:before, -.@{fa-css-prefix}-car:before { content: @fa-var-car; } -.@{fa-css-prefix}-cab:before, -.@{fa-css-prefix}-taxi:before { content: @fa-var-taxi; } -.@{fa-css-prefix}-tree:before { content: @fa-var-tree; } -.@{fa-css-prefix}-spotify:before { content: @fa-var-spotify; } -.@{fa-css-prefix}-deviantart:before { content: @fa-var-deviantart; } -.@{fa-css-prefix}-soundcloud:before { content: @fa-var-soundcloud; } -.@{fa-css-prefix}-database:before { content: @fa-var-database; } -.@{fa-css-prefix}-file-pdf-o:before { content: @fa-var-file-pdf-o; } -.@{fa-css-prefix}-file-word-o:before { content: @fa-var-file-word-o; } -.@{fa-css-prefix}-file-excel-o:before { content: @fa-var-file-excel-o; } -.@{fa-css-prefix}-file-powerpoint-o:before { content: @fa-var-file-powerpoint-o; } -.@{fa-css-prefix}-file-photo-o:before, -.@{fa-css-prefix}-file-picture-o:before, -.@{fa-css-prefix}-file-image-o:before { content: @fa-var-file-image-o; } -.@{fa-css-prefix}-file-zip-o:before, -.@{fa-css-prefix}-file-archive-o:before { content: @fa-var-file-archive-o; } -.@{fa-css-prefix}-file-sound-o:before, -.@{fa-css-prefix}-file-audio-o:before { content: @fa-var-file-audio-o; } -.@{fa-css-prefix}-file-movie-o:before, -.@{fa-css-prefix}-file-video-o:before { content: @fa-var-file-video-o; } -.@{fa-css-prefix}-file-code-o:before { content: @fa-var-file-code-o; } -.@{fa-css-prefix}-vine:before { content: @fa-var-vine; } -.@{fa-css-prefix}-codepen:before { content: @fa-var-codepen; } -.@{fa-css-prefix}-jsfiddle:before { content: @fa-var-jsfiddle; } -.@{fa-css-prefix}-life-bouy:before, -.@{fa-css-prefix}-life-buoy:before, -.@{fa-css-prefix}-life-saver:before, -.@{fa-css-prefix}-support:before, -.@{fa-css-prefix}-life-ring:before { content: @fa-var-life-ring; } -.@{fa-css-prefix}-circle-o-notch:before { content: @fa-var-circle-o-notch; } -.@{fa-css-prefix}-ra:before, -.@{fa-css-prefix}-rebel:before { content: @fa-var-rebel; } -.@{fa-css-prefix}-ge:before, -.@{fa-css-prefix}-empire:before { content: @fa-var-empire; } -.@{fa-css-prefix}-git-square:before { content: @fa-var-git-square; } -.@{fa-css-prefix}-git:before { content: @fa-var-git; } -.@{fa-css-prefix}-y-combinator-square:before, -.@{fa-css-prefix}-yc-square:before, -.@{fa-css-prefix}-hacker-news:before { content: @fa-var-hacker-news; } -.@{fa-css-prefix}-tencent-weibo:before { content: @fa-var-tencent-weibo; } -.@{fa-css-prefix}-qq:before { content: @fa-var-qq; } -.@{fa-css-prefix}-wechat:before, -.@{fa-css-prefix}-weixin:before { content: @fa-var-weixin; } -.@{fa-css-prefix}-send:before, -.@{fa-css-prefix}-paper-plane:before { content: @fa-var-paper-plane; } -.@{fa-css-prefix}-send-o:before, -.@{fa-css-prefix}-paper-plane-o:before { content: @fa-var-paper-plane-o; } -.@{fa-css-prefix}-history:before { content: @fa-var-history; } -.@{fa-css-prefix}-circle-thin:before { content: @fa-var-circle-thin; } -.@{fa-css-prefix}-header:before { content: @fa-var-header; } -.@{fa-css-prefix}-paragraph:before { content: @fa-var-paragraph; } -.@{fa-css-prefix}-sliders:before { content: @fa-var-sliders; } -.@{fa-css-prefix}-share-alt:before { content: @fa-var-share-alt; } -.@{fa-css-prefix}-share-alt-square:before { content: @fa-var-share-alt-square; } -.@{fa-css-prefix}-bomb:before { content: @fa-var-bomb; } -.@{fa-css-prefix}-soccer-ball-o:before, -.@{fa-css-prefix}-futbol-o:before { content: @fa-var-futbol-o; } -.@{fa-css-prefix}-tty:before { content: @fa-var-tty; } -.@{fa-css-prefix}-binoculars:before { content: @fa-var-binoculars; } -.@{fa-css-prefix}-plug:before { content: @fa-var-plug; } -.@{fa-css-prefix}-slideshare:before { content: @fa-var-slideshare; } -.@{fa-css-prefix}-twitch:before { content: @fa-var-twitch; } -.@{fa-css-prefix}-yelp:before { content: @fa-var-yelp; } -.@{fa-css-prefix}-newspaper-o:before { content: @fa-var-newspaper-o; } -.@{fa-css-prefix}-wifi:before { content: @fa-var-wifi; } -.@{fa-css-prefix}-calculator:before { content: @fa-var-calculator; } -.@{fa-css-prefix}-paypal:before { content: @fa-var-paypal; } -.@{fa-css-prefix}-google-wallet:before { content: @fa-var-google-wallet; } -.@{fa-css-prefix}-cc-visa:before { content: @fa-var-cc-visa; } -.@{fa-css-prefix}-cc-mastercard:before { content: @fa-var-cc-mastercard; } -.@{fa-css-prefix}-cc-discover:before { content: @fa-var-cc-discover; } -.@{fa-css-prefix}-cc-amex:before { content: @fa-var-cc-amex; } -.@{fa-css-prefix}-cc-paypal:before { content: @fa-var-cc-paypal; } -.@{fa-css-prefix}-cc-stripe:before { content: @fa-var-cc-stripe; } -.@{fa-css-prefix}-bell-slash:before { content: @fa-var-bell-slash; } -.@{fa-css-prefix}-bell-slash-o:before { content: @fa-var-bell-slash-o; } -.@{fa-css-prefix}-trash:before { content: @fa-var-trash; } -.@{fa-css-prefix}-copyright:before { content: @fa-var-copyright; } -.@{fa-css-prefix}-at:before { content: @fa-var-at; } -.@{fa-css-prefix}-eyedropper:before { content: @fa-var-eyedropper; } -.@{fa-css-prefix}-paint-brush:before { content: @fa-var-paint-brush; } -.@{fa-css-prefix}-birthday-cake:before { content: @fa-var-birthday-cake; } -.@{fa-css-prefix}-area-chart:before { content: @fa-var-area-chart; } -.@{fa-css-prefix}-pie-chart:before { content: @fa-var-pie-chart; } -.@{fa-css-prefix}-line-chart:before { content: @fa-var-line-chart; } -.@{fa-css-prefix}-lastfm:before { content: @fa-var-lastfm; } -.@{fa-css-prefix}-lastfm-square:before { content: @fa-var-lastfm-square; } -.@{fa-css-prefix}-toggle-off:before { content: @fa-var-toggle-off; } -.@{fa-css-prefix}-toggle-on:before { content: @fa-var-toggle-on; } -.@{fa-css-prefix}-bicycle:before { content: @fa-var-bicycle; } -.@{fa-css-prefix}-bus:before { content: @fa-var-bus; } -.@{fa-css-prefix}-ioxhost:before { content: @fa-var-ioxhost; } -.@{fa-css-prefix}-angellist:before { content: @fa-var-angellist; } -.@{fa-css-prefix}-cc:before { content: @fa-var-cc; } -.@{fa-css-prefix}-shekel:before, -.@{fa-css-prefix}-sheqel:before, -.@{fa-css-prefix}-ils:before { content: @fa-var-ils; } -.@{fa-css-prefix}-meanpath:before { content: @fa-var-meanpath; } -.@{fa-css-prefix}-buysellads:before { content: @fa-var-buysellads; } -.@{fa-css-prefix}-connectdevelop:before { content: @fa-var-connectdevelop; } -.@{fa-css-prefix}-dashcube:before { content: @fa-var-dashcube; } -.@{fa-css-prefix}-forumbee:before { content: @fa-var-forumbee; } -.@{fa-css-prefix}-leanpub:before { content: @fa-var-leanpub; } -.@{fa-css-prefix}-sellsy:before { content: @fa-var-sellsy; } -.@{fa-css-prefix}-shirtsinbulk:before { content: @fa-var-shirtsinbulk; } -.@{fa-css-prefix}-simplybuilt:before { content: @fa-var-simplybuilt; } -.@{fa-css-prefix}-skyatlas:before { content: @fa-var-skyatlas; } -.@{fa-css-prefix}-cart-plus:before { content: @fa-var-cart-plus; } -.@{fa-css-prefix}-cart-arrow-down:before { content: @fa-var-cart-arrow-down; } -.@{fa-css-prefix}-diamond:before { content: @fa-var-diamond; } -.@{fa-css-prefix}-ship:before { content: @fa-var-ship; } -.@{fa-css-prefix}-user-secret:before { content: @fa-var-user-secret; } -.@{fa-css-prefix}-motorcycle:before { content: @fa-var-motorcycle; } -.@{fa-css-prefix}-street-view:before { content: @fa-var-street-view; } -.@{fa-css-prefix}-heartbeat:before { content: @fa-var-heartbeat; } -.@{fa-css-prefix}-venus:before { content: @fa-var-venus; } -.@{fa-css-prefix}-mars:before { content: @fa-var-mars; } -.@{fa-css-prefix}-mercury:before { content: @fa-var-mercury; } -.@{fa-css-prefix}-intersex:before, -.@{fa-css-prefix}-transgender:before { content: @fa-var-transgender; } -.@{fa-css-prefix}-transgender-alt:before { content: @fa-var-transgender-alt; } -.@{fa-css-prefix}-venus-double:before { content: @fa-var-venus-double; } -.@{fa-css-prefix}-mars-double:before { content: @fa-var-mars-double; } -.@{fa-css-prefix}-venus-mars:before { content: @fa-var-venus-mars; } -.@{fa-css-prefix}-mars-stroke:before { content: @fa-var-mars-stroke; } -.@{fa-css-prefix}-mars-stroke-v:before { content: @fa-var-mars-stroke-v; } -.@{fa-css-prefix}-mars-stroke-h:before { content: @fa-var-mars-stroke-h; } -.@{fa-css-prefix}-neuter:before { content: @fa-var-neuter; } -.@{fa-css-prefix}-genderless:before { content: @fa-var-genderless; } -.@{fa-css-prefix}-facebook-official:before { content: @fa-var-facebook-official; } -.@{fa-css-prefix}-pinterest-p:before { content: @fa-var-pinterest-p; } -.@{fa-css-prefix}-whatsapp:before { content: @fa-var-whatsapp; } -.@{fa-css-prefix}-server:before { content: @fa-var-server; } -.@{fa-css-prefix}-user-plus:before { content: @fa-var-user-plus; } -.@{fa-css-prefix}-user-times:before { content: @fa-var-user-times; } -.@{fa-css-prefix}-hotel:before, -.@{fa-css-prefix}-bed:before { content: @fa-var-bed; } -.@{fa-css-prefix}-viacoin:before { content: @fa-var-viacoin; } -.@{fa-css-prefix}-train:before { content: @fa-var-train; } -.@{fa-css-prefix}-subway:before { content: @fa-var-subway; } -.@{fa-css-prefix}-medium:before { content: @fa-var-medium; } -.@{fa-css-prefix}-yc:before, -.@{fa-css-prefix}-y-combinator:before { content: @fa-var-y-combinator; } -.@{fa-css-prefix}-optin-monster:before { content: @fa-var-optin-monster; } -.@{fa-css-prefix}-opencart:before { content: @fa-var-opencart; } -.@{fa-css-prefix}-expeditedssl:before { content: @fa-var-expeditedssl; } -.@{fa-css-prefix}-battery-4:before, -.@{fa-css-prefix}-battery-full:before { content: @fa-var-battery-full; } -.@{fa-css-prefix}-battery-3:before, -.@{fa-css-prefix}-battery-three-quarters:before { content: @fa-var-battery-three-quarters; } -.@{fa-css-prefix}-battery-2:before, -.@{fa-css-prefix}-battery-half:before { content: @fa-var-battery-half; } -.@{fa-css-prefix}-battery-1:before, -.@{fa-css-prefix}-battery-quarter:before { content: @fa-var-battery-quarter; } -.@{fa-css-prefix}-battery-0:before, -.@{fa-css-prefix}-battery-empty:before { content: @fa-var-battery-empty; } -.@{fa-css-prefix}-mouse-pointer:before { content: @fa-var-mouse-pointer; } -.@{fa-css-prefix}-i-cursor:before { content: @fa-var-i-cursor; } -.@{fa-css-prefix}-object-group:before { content: @fa-var-object-group; } -.@{fa-css-prefix}-object-ungroup:before { content: @fa-var-object-ungroup; } -.@{fa-css-prefix}-sticky-note:before { content: @fa-var-sticky-note; } -.@{fa-css-prefix}-sticky-note-o:before { content: @fa-var-sticky-note-o; } -.@{fa-css-prefix}-cc-jcb:before { content: @fa-var-cc-jcb; } -.@{fa-css-prefix}-cc-diners-club:before { content: @fa-var-cc-diners-club; } -.@{fa-css-prefix}-clone:before { content: @fa-var-clone; } -.@{fa-css-prefix}-balance-scale:before { content: @fa-var-balance-scale; } -.@{fa-css-prefix}-hourglass-o:before { content: @fa-var-hourglass-o; } -.@{fa-css-prefix}-hourglass-1:before, -.@{fa-css-prefix}-hourglass-start:before { content: @fa-var-hourglass-start; } -.@{fa-css-prefix}-hourglass-2:before, -.@{fa-css-prefix}-hourglass-half:before { content: @fa-var-hourglass-half; } -.@{fa-css-prefix}-hourglass-3:before, -.@{fa-css-prefix}-hourglass-end:before { content: @fa-var-hourglass-end; } -.@{fa-css-prefix}-hourglass:before { content: @fa-var-hourglass; } -.@{fa-css-prefix}-hand-grab-o:before, -.@{fa-css-prefix}-hand-rock-o:before { content: @fa-var-hand-rock-o; } -.@{fa-css-prefix}-hand-stop-o:before, -.@{fa-css-prefix}-hand-paper-o:before { content: @fa-var-hand-paper-o; } -.@{fa-css-prefix}-hand-scissors-o:before { content: @fa-var-hand-scissors-o; } -.@{fa-css-prefix}-hand-lizard-o:before { content: @fa-var-hand-lizard-o; } -.@{fa-css-prefix}-hand-spock-o:before { content: @fa-var-hand-spock-o; } -.@{fa-css-prefix}-hand-pointer-o:before { content: @fa-var-hand-pointer-o; } -.@{fa-css-prefix}-hand-peace-o:before { content: @fa-var-hand-peace-o; } -.@{fa-css-prefix}-trademark:before { content: @fa-var-trademark; } -.@{fa-css-prefix}-registered:before { content: @fa-var-registered; } -.@{fa-css-prefix}-creative-commons:before { content: @fa-var-creative-commons; } -.@{fa-css-prefix}-gg:before { content: @fa-var-gg; } -.@{fa-css-prefix}-gg-circle:before { content: @fa-var-gg-circle; } -.@{fa-css-prefix}-tripadvisor:before { content: @fa-var-tripadvisor; } -.@{fa-css-prefix}-odnoklassniki:before { content: @fa-var-odnoklassniki; } -.@{fa-css-prefix}-odnoklassniki-square:before { content: @fa-var-odnoklassniki-square; } -.@{fa-css-prefix}-get-pocket:before { content: @fa-var-get-pocket; } -.@{fa-css-prefix}-wikipedia-w:before { content: @fa-var-wikipedia-w; } -.@{fa-css-prefix}-safari:before { content: @fa-var-safari; } -.@{fa-css-prefix}-chrome:before { content: @fa-var-chrome; } -.@{fa-css-prefix}-firefox:before { content: @fa-var-firefox; } -.@{fa-css-prefix}-opera:before { content: @fa-var-opera; } -.@{fa-css-prefix}-internet-explorer:before { content: @fa-var-internet-explorer; } -.@{fa-css-prefix}-tv:before, -.@{fa-css-prefix}-television:before { content: @fa-var-television; } -.@{fa-css-prefix}-contao:before { content: @fa-var-contao; } -.@{fa-css-prefix}-500px:before { content: @fa-var-500px; } -.@{fa-css-prefix}-amazon:before { content: @fa-var-amazon; } -.@{fa-css-prefix}-calendar-plus-o:before { content: @fa-var-calendar-plus-o; } -.@{fa-css-prefix}-calendar-minus-o:before { content: @fa-var-calendar-minus-o; } -.@{fa-css-prefix}-calendar-times-o:before { content: @fa-var-calendar-times-o; } -.@{fa-css-prefix}-calendar-check-o:before { content: @fa-var-calendar-check-o; } -.@{fa-css-prefix}-industry:before { content: @fa-var-industry; } -.@{fa-css-prefix}-map-pin:before { content: @fa-var-map-pin; } -.@{fa-css-prefix}-map-signs:before { content: @fa-var-map-signs; } -.@{fa-css-prefix}-map-o:before { content: @fa-var-map-o; } -.@{fa-css-prefix}-map:before { content: @fa-var-map; } -.@{fa-css-prefix}-commenting:before { content: @fa-var-commenting; } -.@{fa-css-prefix}-commenting-o:before { content: @fa-var-commenting-o; } -.@{fa-css-prefix}-houzz:before { content: @fa-var-houzz; } -.@{fa-css-prefix}-vimeo:before { content: @fa-var-vimeo; } -.@{fa-css-prefix}-black-tie:before { content: @fa-var-black-tie; } -.@{fa-css-prefix}-fonticons:before { content: @fa-var-fonticons; } -.@{fa-css-prefix}-reddit-alien:before { content: @fa-var-reddit-alien; } -.@{fa-css-prefix}-edge:before { content: @fa-var-edge; } -.@{fa-css-prefix}-credit-card-alt:before { content: @fa-var-credit-card-alt; } -.@{fa-css-prefix}-codiepie:before { content: @fa-var-codiepie; } -.@{fa-css-prefix}-modx:before { content: @fa-var-modx; } -.@{fa-css-prefix}-fort-awesome:before { content: @fa-var-fort-awesome; } -.@{fa-css-prefix}-usb:before { content: @fa-var-usb; } -.@{fa-css-prefix}-product-hunt:before { content: @fa-var-product-hunt; } -.@{fa-css-prefix}-mixcloud:before { content: @fa-var-mixcloud; } -.@{fa-css-prefix}-scribd:before { content: @fa-var-scribd; } -.@{fa-css-prefix}-pause-circle:before { content: @fa-var-pause-circle; } -.@{fa-css-prefix}-pause-circle-o:before { content: @fa-var-pause-circle-o; } -.@{fa-css-prefix}-stop-circle:before { content: @fa-var-stop-circle; } -.@{fa-css-prefix}-stop-circle-o:before { content: @fa-var-stop-circle-o; } -.@{fa-css-prefix}-shopping-bag:before { content: @fa-var-shopping-bag; } -.@{fa-css-prefix}-shopping-basket:before { content: @fa-var-shopping-basket; } -.@{fa-css-prefix}-hashtag:before { content: @fa-var-hashtag; } -.@{fa-css-prefix}-bluetooth:before { content: @fa-var-bluetooth; } -.@{fa-css-prefix}-bluetooth-b:before { content: @fa-var-bluetooth-b; } -.@{fa-css-prefix}-percent:before { content: @fa-var-percent; } -.@{fa-css-prefix}-gitlab:before { content: @fa-var-gitlab; } -.@{fa-css-prefix}-wpbeginner:before { content: @fa-var-wpbeginner; } -.@{fa-css-prefix}-wpforms:before { content: @fa-var-wpforms; } -.@{fa-css-prefix}-envira:before { content: @fa-var-envira; } -.@{fa-css-prefix}-universal-access:before { content: @fa-var-universal-access; } -.@{fa-css-prefix}-wheelchair-alt:before { content: @fa-var-wheelchair-alt; } -.@{fa-css-prefix}-question-circle-o:before { content: @fa-var-question-circle-o; } -.@{fa-css-prefix}-blind:before { content: @fa-var-blind; } -.@{fa-css-prefix}-audio-description:before { content: @fa-var-audio-description; } -.@{fa-css-prefix}-volume-control-phone:before { content: @fa-var-volume-control-phone; } -.@{fa-css-prefix}-braille:before { content: @fa-var-braille; } -.@{fa-css-prefix}-assistive-listening-systems:before { content: @fa-var-assistive-listening-systems; } -.@{fa-css-prefix}-asl-interpreting:before, -.@{fa-css-prefix}-american-sign-language-interpreting:before { content: @fa-var-american-sign-language-interpreting; } -.@{fa-css-prefix}-deafness:before, -.@{fa-css-prefix}-hard-of-hearing:before, -.@{fa-css-prefix}-deaf:before { content: @fa-var-deaf; } -.@{fa-css-prefix}-glide:before { content: @fa-var-glide; } -.@{fa-css-prefix}-glide-g:before { content: @fa-var-glide-g; } -.@{fa-css-prefix}-signing:before, -.@{fa-css-prefix}-sign-language:before { content: @fa-var-sign-language; } -.@{fa-css-prefix}-low-vision:before { content: @fa-var-low-vision; } -.@{fa-css-prefix}-viadeo:before { content: @fa-var-viadeo; } -.@{fa-css-prefix}-viadeo-square:before { content: @fa-var-viadeo-square; } -.@{fa-css-prefix}-snapchat:before { content: @fa-var-snapchat; } -.@{fa-css-prefix}-snapchat-ghost:before { content: @fa-var-snapchat-ghost; } -.@{fa-css-prefix}-snapchat-square:before { content: @fa-var-snapchat-square; } diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/less/larger.less b/iam-login-service/src/main/webapp/resources/font-awesome/less/larger.less deleted file mode 100644 index c9d646770..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/less/larger.less +++ /dev/null @@ -1,13 +0,0 @@ -// Icon Sizes -// ------------------------- - -/* makes the font 33% larger relative to the icon container */ -.@{fa-css-prefix}-lg { - font-size: (4em / 3); - line-height: (3em / 4); - vertical-align: -15%; -} -.@{fa-css-prefix}-2x { font-size: 2em; } -.@{fa-css-prefix}-3x { font-size: 3em; } -.@{fa-css-prefix}-4x { font-size: 4em; } -.@{fa-css-prefix}-5x { font-size: 5em; } diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/less/list.less b/iam-login-service/src/main/webapp/resources/font-awesome/less/list.less deleted file mode 100644 index 0b440382f..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/less/list.less +++ /dev/null @@ -1,19 +0,0 @@ -// List Icons -// ------------------------- - -.@{fa-css-prefix}-ul { - padding-left: 0; - margin-left: @fa-li-width; - list-style-type: none; - > li { position: relative; } -} -.@{fa-css-prefix}-li { - position: absolute; - left: -@fa-li-width; - width: @fa-li-width; - top: (2em / 14); - text-align: center; - &.@{fa-css-prefix}-lg { - left: (-@fa-li-width + (4em / 14)); - } -} diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/less/mixins.less b/iam-login-service/src/main/webapp/resources/font-awesome/less/mixins.less deleted file mode 100644 index beef231d0..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/less/mixins.less +++ /dev/null @@ -1,60 +0,0 @@ -// Mixins -// -------------------------- - -.fa-icon() { - display: inline-block; - font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration - font-size: inherit; // can't have font-size inherit on line above, so need to override - text-rendering: auto; // optimizelegibility throws things off #1094 - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -} - -.fa-icon-rotate(@degrees, @rotation) { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})"; - -webkit-transform: rotate(@degrees); - -ms-transform: rotate(@degrees); - transform: rotate(@degrees); -} - -.fa-icon-flip(@horiz, @vert, @rotation) { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)"; - -webkit-transform: scale(@horiz, @vert); - -ms-transform: scale(@horiz, @vert); - transform: scale(@horiz, @vert); -} - - -// Only display content to screen readers. A la Bootstrap 4. -// -// See: http://a11yproject.com/posts/how-to-hide-content/ - -.sr-only() { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0,0,0,0); - border: 0; -} - -// Use in conjunction with .sr-only to only display content when it's focused. -// -// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 -// -// Credit: HTML5 Boilerplate - -.sr-only-focusable() { - &:active, - &:focus { - position: static; - width: auto; - height: auto; - margin: 0; - overflow: visible; - clip: auto; - } -} diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/less/path.less b/iam-login-service/src/main/webapp/resources/font-awesome/less/path.less deleted file mode 100644 index 835be41f8..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/less/path.less +++ /dev/null @@ -1,15 +0,0 @@ -/* FONT PATH - * -------------------------- */ - -@font-face { - font-family: 'FontAwesome'; - src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); - src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), - url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), - url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), - url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), - url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); - // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts - font-weight: normal; - font-style: normal; -} diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/less/rotated-flipped.less b/iam-login-service/src/main/webapp/resources/font-awesome/less/rotated-flipped.less deleted file mode 100644 index f6ba81475..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/less/rotated-flipped.less +++ /dev/null @@ -1,20 +0,0 @@ -// Rotated & Flipped Icons -// ------------------------- - -.@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } -.@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } -.@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } - -.@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } -.@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } - -// Hook for IE8-9 -// ------------------------- - -:root .@{fa-css-prefix}-rotate-90, -:root .@{fa-css-prefix}-rotate-180, -:root .@{fa-css-prefix}-rotate-270, -:root .@{fa-css-prefix}-flip-horizontal, -:root .@{fa-css-prefix}-flip-vertical { - filter: none; -} diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/less/screen-reader.less b/iam-login-service/src/main/webapp/resources/font-awesome/less/screen-reader.less deleted file mode 100644 index 11c188196..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/less/screen-reader.less +++ /dev/null @@ -1,5 +0,0 @@ -// Screen Readers -// ------------------------- - -.sr-only { .sr-only(); } -.sr-only-focusable { .sr-only-focusable(); } diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/less/stacked.less b/iam-login-service/src/main/webapp/resources/font-awesome/less/stacked.less deleted file mode 100644 index fc53fb0e7..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/less/stacked.less +++ /dev/null @@ -1,20 +0,0 @@ -// Stacked Icons -// ------------------------- - -.@{fa-css-prefix}-stack { - position: relative; - display: inline-block; - width: 2em; - height: 2em; - line-height: 2em; - vertical-align: middle; -} -.@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { - position: absolute; - left: 0; - width: 100%; - text-align: center; -} -.@{fa-css-prefix}-stack-1x { line-height: inherit; } -.@{fa-css-prefix}-stack-2x { font-size: 2em; } -.@{fa-css-prefix}-inverse { color: @fa-inverse; } diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/less/variables.less b/iam-login-service/src/main/webapp/resources/font-awesome/less/variables.less deleted file mode 100644 index 8118e8f70..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/less/variables.less +++ /dev/null @@ -1,735 +0,0 @@ -// Variables -// -------------------------- - -@fa-font-path: "../fonts"; -@fa-font-size-base: 14px; -@fa-line-height-base: 1; -//@fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.6.1/fonts"; // for referencing Bootstrap CDN font files directly -@fa-css-prefix: fa; -@fa-version: "4.6.1"; -@fa-border-color: #eee; -@fa-inverse: #fff; -@fa-li-width: (30em / 14); - -@fa-var-500px: "\f26e"; -@fa-var-adjust: "\f042"; -@fa-var-adn: "\f170"; -@fa-var-align-center: "\f037"; -@fa-var-align-justify: "\f039"; -@fa-var-align-left: "\f036"; -@fa-var-align-right: "\f038"; -@fa-var-amazon: "\f270"; -@fa-var-ambulance: "\f0f9"; -@fa-var-american-sign-language-interpreting: "\f2a3"; -@fa-var-anchor: "\f13d"; -@fa-var-android: "\f17b"; -@fa-var-angellist: "\f209"; -@fa-var-angle-double-down: "\f103"; -@fa-var-angle-double-left: "\f100"; -@fa-var-angle-double-right: "\f101"; -@fa-var-angle-double-up: "\f102"; -@fa-var-angle-down: "\f107"; -@fa-var-angle-left: "\f104"; -@fa-var-angle-right: "\f105"; -@fa-var-angle-up: "\f106"; -@fa-var-apple: "\f179"; -@fa-var-archive: "\f187"; -@fa-var-area-chart: "\f1fe"; -@fa-var-arrow-circle-down: "\f0ab"; -@fa-var-arrow-circle-left: "\f0a8"; -@fa-var-arrow-circle-o-down: "\f01a"; -@fa-var-arrow-circle-o-left: "\f190"; -@fa-var-arrow-circle-o-right: "\f18e"; -@fa-var-arrow-circle-o-up: "\f01b"; -@fa-var-arrow-circle-right: "\f0a9"; -@fa-var-arrow-circle-up: "\f0aa"; -@fa-var-arrow-down: "\f063"; -@fa-var-arrow-left: "\f060"; -@fa-var-arrow-right: "\f061"; -@fa-var-arrow-up: "\f062"; -@fa-var-arrows: "\f047"; -@fa-var-arrows-alt: "\f0b2"; -@fa-var-arrows-h: "\f07e"; -@fa-var-arrows-v: "\f07d"; -@fa-var-asl-interpreting: "\f2a3"; -@fa-var-assistive-listening-systems: "\f2a2"; -@fa-var-asterisk: "\f069"; -@fa-var-at: "\f1fa"; -@fa-var-audio-description: "\f29e"; -@fa-var-automobile: "\f1b9"; -@fa-var-backward: "\f04a"; -@fa-var-balance-scale: "\f24e"; -@fa-var-ban: "\f05e"; -@fa-var-bank: "\f19c"; -@fa-var-bar-chart: "\f080"; -@fa-var-bar-chart-o: "\f080"; -@fa-var-barcode: "\f02a"; -@fa-var-bars: "\f0c9"; -@fa-var-battery-0: "\f244"; -@fa-var-battery-1: "\f243"; -@fa-var-battery-2: "\f242"; -@fa-var-battery-3: "\f241"; -@fa-var-battery-4: "\f240"; -@fa-var-battery-empty: "\f244"; -@fa-var-battery-full: "\f240"; -@fa-var-battery-half: "\f242"; -@fa-var-battery-quarter: "\f243"; -@fa-var-battery-three-quarters: "\f241"; -@fa-var-bed: "\f236"; -@fa-var-beer: "\f0fc"; -@fa-var-behance: "\f1b4"; -@fa-var-behance-square: "\f1b5"; -@fa-var-bell: "\f0f3"; -@fa-var-bell-o: "\f0a2"; -@fa-var-bell-slash: "\f1f6"; -@fa-var-bell-slash-o: "\f1f7"; -@fa-var-bicycle: "\f206"; -@fa-var-binoculars: "\f1e5"; -@fa-var-birthday-cake: "\f1fd"; -@fa-var-bitbucket: "\f171"; -@fa-var-bitbucket-square: "\f172"; -@fa-var-bitcoin: "\f15a"; -@fa-var-black-tie: "\f27e"; -@fa-var-blind: "\f29d"; -@fa-var-bluetooth: "\f293"; -@fa-var-bluetooth-b: "\f294"; -@fa-var-bold: "\f032"; -@fa-var-bolt: "\f0e7"; -@fa-var-bomb: "\f1e2"; -@fa-var-book: "\f02d"; -@fa-var-bookmark: "\f02e"; -@fa-var-bookmark-o: "\f097"; -@fa-var-braille: "\f2a1"; -@fa-var-briefcase: "\f0b1"; -@fa-var-btc: "\f15a"; -@fa-var-bug: "\f188"; -@fa-var-building: "\f1ad"; -@fa-var-building-o: "\f0f7"; -@fa-var-bullhorn: "\f0a1"; -@fa-var-bullseye: "\f140"; -@fa-var-bus: "\f207"; -@fa-var-buysellads: "\f20d"; -@fa-var-cab: "\f1ba"; -@fa-var-calculator: "\f1ec"; -@fa-var-calendar: "\f073"; -@fa-var-calendar-check-o: "\f274"; -@fa-var-calendar-minus-o: "\f272"; -@fa-var-calendar-o: "\f133"; -@fa-var-calendar-plus-o: "\f271"; -@fa-var-calendar-times-o: "\f273"; -@fa-var-camera: "\f030"; -@fa-var-camera-retro: "\f083"; -@fa-var-car: "\f1b9"; -@fa-var-caret-down: "\f0d7"; -@fa-var-caret-left: "\f0d9"; -@fa-var-caret-right: "\f0da"; -@fa-var-caret-square-o-down: "\f150"; -@fa-var-caret-square-o-left: "\f191"; -@fa-var-caret-square-o-right: "\f152"; -@fa-var-caret-square-o-up: "\f151"; -@fa-var-caret-up: "\f0d8"; -@fa-var-cart-arrow-down: "\f218"; -@fa-var-cart-plus: "\f217"; -@fa-var-cc: "\f20a"; -@fa-var-cc-amex: "\f1f3"; -@fa-var-cc-diners-club: "\f24c"; -@fa-var-cc-discover: "\f1f2"; -@fa-var-cc-jcb: "\f24b"; -@fa-var-cc-mastercard: "\f1f1"; -@fa-var-cc-paypal: "\f1f4"; -@fa-var-cc-stripe: "\f1f5"; -@fa-var-cc-visa: "\f1f0"; -@fa-var-certificate: "\f0a3"; -@fa-var-chain: "\f0c1"; -@fa-var-chain-broken: "\f127"; -@fa-var-check: "\f00c"; -@fa-var-check-circle: "\f058"; -@fa-var-check-circle-o: "\f05d"; -@fa-var-check-square: "\f14a"; -@fa-var-check-square-o: "\f046"; -@fa-var-chevron-circle-down: "\f13a"; -@fa-var-chevron-circle-left: "\f137"; -@fa-var-chevron-circle-right: "\f138"; -@fa-var-chevron-circle-up: "\f139"; -@fa-var-chevron-down: "\f078"; -@fa-var-chevron-left: "\f053"; -@fa-var-chevron-right: "\f054"; -@fa-var-chevron-up: "\f077"; -@fa-var-child: "\f1ae"; -@fa-var-chrome: "\f268"; -@fa-var-circle: "\f111"; -@fa-var-circle-o: "\f10c"; -@fa-var-circle-o-notch: "\f1ce"; -@fa-var-circle-thin: "\f1db"; -@fa-var-clipboard: "\f0ea"; -@fa-var-clock-o: "\f017"; -@fa-var-clone: "\f24d"; -@fa-var-close: "\f00d"; -@fa-var-cloud: "\f0c2"; -@fa-var-cloud-download: "\f0ed"; -@fa-var-cloud-upload: "\f0ee"; -@fa-var-cny: "\f157"; -@fa-var-code: "\f121"; -@fa-var-code-fork: "\f126"; -@fa-var-codepen: "\f1cb"; -@fa-var-codiepie: "\f284"; -@fa-var-coffee: "\f0f4"; -@fa-var-cog: "\f013"; -@fa-var-cogs: "\f085"; -@fa-var-columns: "\f0db"; -@fa-var-comment: "\f075"; -@fa-var-comment-o: "\f0e5"; -@fa-var-commenting: "\f27a"; -@fa-var-commenting-o: "\f27b"; -@fa-var-comments: "\f086"; -@fa-var-comments-o: "\f0e6"; -@fa-var-compass: "\f14e"; -@fa-var-compress: "\f066"; -@fa-var-connectdevelop: "\f20e"; -@fa-var-contao: "\f26d"; -@fa-var-copy: "\f0c5"; -@fa-var-copyright: "\f1f9"; -@fa-var-creative-commons: "\f25e"; -@fa-var-credit-card: "\f09d"; -@fa-var-credit-card-alt: "\f283"; -@fa-var-crop: "\f125"; -@fa-var-crosshairs: "\f05b"; -@fa-var-css3: "\f13c"; -@fa-var-cube: "\f1b2"; -@fa-var-cubes: "\f1b3"; -@fa-var-cut: "\f0c4"; -@fa-var-cutlery: "\f0f5"; -@fa-var-dashboard: "\f0e4"; -@fa-var-dashcube: "\f210"; -@fa-var-database: "\f1c0"; -@fa-var-deaf: "\f2a4"; -@fa-var-deafness: "\f2a4"; -@fa-var-dedent: "\f03b"; -@fa-var-delicious: "\f1a5"; -@fa-var-desktop: "\f108"; -@fa-var-deviantart: "\f1bd"; -@fa-var-diamond: "\f219"; -@fa-var-digg: "\f1a6"; -@fa-var-dollar: "\f155"; -@fa-var-dot-circle-o: "\f192"; -@fa-var-download: "\f019"; -@fa-var-dribbble: "\f17d"; -@fa-var-dropbox: "\f16b"; -@fa-var-drupal: "\f1a9"; -@fa-var-edge: "\f282"; -@fa-var-edit: "\f044"; -@fa-var-eject: "\f052"; -@fa-var-ellipsis-h: "\f141"; -@fa-var-ellipsis-v: "\f142"; -@fa-var-empire: "\f1d1"; -@fa-var-envelope: "\f0e0"; -@fa-var-envelope-o: "\f003"; -@fa-var-envelope-square: "\f199"; -@fa-var-envira: "\f299"; -@fa-var-eraser: "\f12d"; -@fa-var-eur: "\f153"; -@fa-var-euro: "\f153"; -@fa-var-exchange: "\f0ec"; -@fa-var-exclamation: "\f12a"; -@fa-var-exclamation-circle: "\f06a"; -@fa-var-exclamation-triangle: "\f071"; -@fa-var-expand: "\f065"; -@fa-var-expeditedssl: "\f23e"; -@fa-var-external-link: "\f08e"; -@fa-var-external-link-square: "\f14c"; -@fa-var-eye: "\f06e"; -@fa-var-eye-slash: "\f070"; -@fa-var-eyedropper: "\f1fb"; -@fa-var-facebook: "\f09a"; -@fa-var-facebook-f: "\f09a"; -@fa-var-facebook-official: "\f230"; -@fa-var-facebook-square: "\f082"; -@fa-var-fast-backward: "\f049"; -@fa-var-fast-forward: "\f050"; -@fa-var-fax: "\f1ac"; -@fa-var-feed: "\f09e"; -@fa-var-female: "\f182"; -@fa-var-fighter-jet: "\f0fb"; -@fa-var-file: "\f15b"; -@fa-var-file-archive-o: "\f1c6"; -@fa-var-file-audio-o: "\f1c7"; -@fa-var-file-code-o: "\f1c9"; -@fa-var-file-excel-o: "\f1c3"; -@fa-var-file-image-o: "\f1c5"; -@fa-var-file-movie-o: "\f1c8"; -@fa-var-file-o: "\f016"; -@fa-var-file-pdf-o: "\f1c1"; -@fa-var-file-photo-o: "\f1c5"; -@fa-var-file-picture-o: "\f1c5"; -@fa-var-file-powerpoint-o: "\f1c4"; -@fa-var-file-sound-o: "\f1c7"; -@fa-var-file-text: "\f15c"; -@fa-var-file-text-o: "\f0f6"; -@fa-var-file-video-o: "\f1c8"; -@fa-var-file-word-o: "\f1c2"; -@fa-var-file-zip-o: "\f1c6"; -@fa-var-files-o: "\f0c5"; -@fa-var-film: "\f008"; -@fa-var-filter: "\f0b0"; -@fa-var-fire: "\f06d"; -@fa-var-fire-extinguisher: "\f134"; -@fa-var-firefox: "\f269"; -@fa-var-flag: "\f024"; -@fa-var-flag-checkered: "\f11e"; -@fa-var-flag-o: "\f11d"; -@fa-var-flash: "\f0e7"; -@fa-var-flask: "\f0c3"; -@fa-var-flickr: "\f16e"; -@fa-var-floppy-o: "\f0c7"; -@fa-var-folder: "\f07b"; -@fa-var-folder-o: "\f114"; -@fa-var-folder-open: "\f07c"; -@fa-var-folder-open-o: "\f115"; -@fa-var-font: "\f031"; -@fa-var-fonticons: "\f280"; -@fa-var-fort-awesome: "\f286"; -@fa-var-forumbee: "\f211"; -@fa-var-forward: "\f04e"; -@fa-var-foursquare: "\f180"; -@fa-var-frown-o: "\f119"; -@fa-var-futbol-o: "\f1e3"; -@fa-var-gamepad: "\f11b"; -@fa-var-gavel: "\f0e3"; -@fa-var-gbp: "\f154"; -@fa-var-ge: "\f1d1"; -@fa-var-gear: "\f013"; -@fa-var-gears: "\f085"; -@fa-var-genderless: "\f22d"; -@fa-var-get-pocket: "\f265"; -@fa-var-gg: "\f260"; -@fa-var-gg-circle: "\f261"; -@fa-var-gift: "\f06b"; -@fa-var-git: "\f1d3"; -@fa-var-git-square: "\f1d2"; -@fa-var-github: "\f09b"; -@fa-var-github-alt: "\f113"; -@fa-var-github-square: "\f092"; -@fa-var-gitlab: "\f296"; -@fa-var-gittip: "\f184"; -@fa-var-glass: "\f000"; -@fa-var-glide: "\f2a5"; -@fa-var-glide-g: "\f2a6"; -@fa-var-globe: "\f0ac"; -@fa-var-google: "\f1a0"; -@fa-var-google-plus: "\f0d5"; -@fa-var-google-plus-square: "\f0d4"; -@fa-var-google-wallet: "\f1ee"; -@fa-var-graduation-cap: "\f19d"; -@fa-var-gratipay: "\f184"; -@fa-var-group: "\f0c0"; -@fa-var-h-square: "\f0fd"; -@fa-var-hacker-news: "\f1d4"; -@fa-var-hand-grab-o: "\f255"; -@fa-var-hand-lizard-o: "\f258"; -@fa-var-hand-o-down: "\f0a7"; -@fa-var-hand-o-left: "\f0a5"; -@fa-var-hand-o-right: "\f0a4"; -@fa-var-hand-o-up: "\f0a6"; -@fa-var-hand-paper-o: "\f256"; -@fa-var-hand-peace-o: "\f25b"; -@fa-var-hand-pointer-o: "\f25a"; -@fa-var-hand-rock-o: "\f255"; -@fa-var-hand-scissors-o: "\f257"; -@fa-var-hand-spock-o: "\f259"; -@fa-var-hand-stop-o: "\f256"; -@fa-var-hard-of-hearing: "\f2a4"; -@fa-var-hashtag: "\f292"; -@fa-var-hdd-o: "\f0a0"; -@fa-var-header: "\f1dc"; -@fa-var-headphones: "\f025"; -@fa-var-heart: "\f004"; -@fa-var-heart-o: "\f08a"; -@fa-var-heartbeat: "\f21e"; -@fa-var-history: "\f1da"; -@fa-var-home: "\f015"; -@fa-var-hospital-o: "\f0f8"; -@fa-var-hotel: "\f236"; -@fa-var-hourglass: "\f254"; -@fa-var-hourglass-1: "\f251"; -@fa-var-hourglass-2: "\f252"; -@fa-var-hourglass-3: "\f253"; -@fa-var-hourglass-end: "\f253"; -@fa-var-hourglass-half: "\f252"; -@fa-var-hourglass-o: "\f250"; -@fa-var-hourglass-start: "\f251"; -@fa-var-houzz: "\f27c"; -@fa-var-html5: "\f13b"; -@fa-var-i-cursor: "\f246"; -@fa-var-ils: "\f20b"; -@fa-var-image: "\f03e"; -@fa-var-inbox: "\f01c"; -@fa-var-indent: "\f03c"; -@fa-var-industry: "\f275"; -@fa-var-info: "\f129"; -@fa-var-info-circle: "\f05a"; -@fa-var-inr: "\f156"; -@fa-var-instagram: "\f16d"; -@fa-var-institution: "\f19c"; -@fa-var-internet-explorer: "\f26b"; -@fa-var-intersex: "\f224"; -@fa-var-ioxhost: "\f208"; -@fa-var-italic: "\f033"; -@fa-var-joomla: "\f1aa"; -@fa-var-jpy: "\f157"; -@fa-var-jsfiddle: "\f1cc"; -@fa-var-key: "\f084"; -@fa-var-keyboard-o: "\f11c"; -@fa-var-krw: "\f159"; -@fa-var-language: "\f1ab"; -@fa-var-laptop: "\f109"; -@fa-var-lastfm: "\f202"; -@fa-var-lastfm-square: "\f203"; -@fa-var-leaf: "\f06c"; -@fa-var-leanpub: "\f212"; -@fa-var-legal: "\f0e3"; -@fa-var-lemon-o: "\f094"; -@fa-var-level-down: "\f149"; -@fa-var-level-up: "\f148"; -@fa-var-life-bouy: "\f1cd"; -@fa-var-life-buoy: "\f1cd"; -@fa-var-life-ring: "\f1cd"; -@fa-var-life-saver: "\f1cd"; -@fa-var-lightbulb-o: "\f0eb"; -@fa-var-line-chart: "\f201"; -@fa-var-link: "\f0c1"; -@fa-var-linkedin: "\f0e1"; -@fa-var-linkedin-square: "\f08c"; -@fa-var-linux: "\f17c"; -@fa-var-list: "\f03a"; -@fa-var-list-alt: "\f022"; -@fa-var-list-ol: "\f0cb"; -@fa-var-list-ul: "\f0ca"; -@fa-var-location-arrow: "\f124"; -@fa-var-lock: "\f023"; -@fa-var-long-arrow-down: "\f175"; -@fa-var-long-arrow-left: "\f177"; -@fa-var-long-arrow-right: "\f178"; -@fa-var-long-arrow-up: "\f176"; -@fa-var-low-vision: "\f2a8"; -@fa-var-magic: "\f0d0"; -@fa-var-magnet: "\f076"; -@fa-var-mail-forward: "\f064"; -@fa-var-mail-reply: "\f112"; -@fa-var-mail-reply-all: "\f122"; -@fa-var-male: "\f183"; -@fa-var-map: "\f279"; -@fa-var-map-marker: "\f041"; -@fa-var-map-o: "\f278"; -@fa-var-map-pin: "\f276"; -@fa-var-map-signs: "\f277"; -@fa-var-mars: "\f222"; -@fa-var-mars-double: "\f227"; -@fa-var-mars-stroke: "\f229"; -@fa-var-mars-stroke-h: "\f22b"; -@fa-var-mars-stroke-v: "\f22a"; -@fa-var-maxcdn: "\f136"; -@fa-var-meanpath: "\f20c"; -@fa-var-medium: "\f23a"; -@fa-var-medkit: "\f0fa"; -@fa-var-meh-o: "\f11a"; -@fa-var-mercury: "\f223"; -@fa-var-microphone: "\f130"; -@fa-var-microphone-slash: "\f131"; -@fa-var-minus: "\f068"; -@fa-var-minus-circle: "\f056"; -@fa-var-minus-square: "\f146"; -@fa-var-minus-square-o: "\f147"; -@fa-var-mixcloud: "\f289"; -@fa-var-mobile: "\f10b"; -@fa-var-mobile-phone: "\f10b"; -@fa-var-modx: "\f285"; -@fa-var-money: "\f0d6"; -@fa-var-moon-o: "\f186"; -@fa-var-mortar-board: "\f19d"; -@fa-var-motorcycle: "\f21c"; -@fa-var-mouse-pointer: "\f245"; -@fa-var-music: "\f001"; -@fa-var-navicon: "\f0c9"; -@fa-var-neuter: "\f22c"; -@fa-var-newspaper-o: "\f1ea"; -@fa-var-object-group: "\f247"; -@fa-var-object-ungroup: "\f248"; -@fa-var-odnoklassniki: "\f263"; -@fa-var-odnoklassniki-square: "\f264"; -@fa-var-opencart: "\f23d"; -@fa-var-openid: "\f19b"; -@fa-var-opera: "\f26a"; -@fa-var-optin-monster: "\f23c"; -@fa-var-outdent: "\f03b"; -@fa-var-pagelines: "\f18c"; -@fa-var-paint-brush: "\f1fc"; -@fa-var-paper-plane: "\f1d8"; -@fa-var-paper-plane-o: "\f1d9"; -@fa-var-paperclip: "\f0c6"; -@fa-var-paragraph: "\f1dd"; -@fa-var-paste: "\f0ea"; -@fa-var-pause: "\f04c"; -@fa-var-pause-circle: "\f28b"; -@fa-var-pause-circle-o: "\f28c"; -@fa-var-paw: "\f1b0"; -@fa-var-paypal: "\f1ed"; -@fa-var-pencil: "\f040"; -@fa-var-pencil-square: "\f14b"; -@fa-var-pencil-square-o: "\f044"; -@fa-var-percent: "\f295"; -@fa-var-phone: "\f095"; -@fa-var-phone-square: "\f098"; -@fa-var-photo: "\f03e"; -@fa-var-picture-o: "\f03e"; -@fa-var-pie-chart: "\f200"; -@fa-var-pied-piper: "\f1a7"; -@fa-var-pied-piper-alt: "\f1a8"; -@fa-var-pinterest: "\f0d2"; -@fa-var-pinterest-p: "\f231"; -@fa-var-pinterest-square: "\f0d3"; -@fa-var-plane: "\f072"; -@fa-var-play: "\f04b"; -@fa-var-play-circle: "\f144"; -@fa-var-play-circle-o: "\f01d"; -@fa-var-plug: "\f1e6"; -@fa-var-plus: "\f067"; -@fa-var-plus-circle: "\f055"; -@fa-var-plus-square: "\f0fe"; -@fa-var-plus-square-o: "\f196"; -@fa-var-power-off: "\f011"; -@fa-var-print: "\f02f"; -@fa-var-product-hunt: "\f288"; -@fa-var-puzzle-piece: "\f12e"; -@fa-var-qq: "\f1d6"; -@fa-var-qrcode: "\f029"; -@fa-var-question: "\f128"; -@fa-var-question-circle: "\f059"; -@fa-var-question-circle-o: "\f29c"; -@fa-var-quote-left: "\f10d"; -@fa-var-quote-right: "\f10e"; -@fa-var-ra: "\f1d0"; -@fa-var-random: "\f074"; -@fa-var-rebel: "\f1d0"; -@fa-var-recycle: "\f1b8"; -@fa-var-reddit: "\f1a1"; -@fa-var-reddit-alien: "\f281"; -@fa-var-reddit-square: "\f1a2"; -@fa-var-refresh: "\f021"; -@fa-var-registered: "\f25d"; -@fa-var-remove: "\f00d"; -@fa-var-renren: "\f18b"; -@fa-var-reorder: "\f0c9"; -@fa-var-repeat: "\f01e"; -@fa-var-reply: "\f112"; -@fa-var-reply-all: "\f122"; -@fa-var-retweet: "\f079"; -@fa-var-rmb: "\f157"; -@fa-var-road: "\f018"; -@fa-var-rocket: "\f135"; -@fa-var-rotate-left: "\f0e2"; -@fa-var-rotate-right: "\f01e"; -@fa-var-rouble: "\f158"; -@fa-var-rss: "\f09e"; -@fa-var-rss-square: "\f143"; -@fa-var-rub: "\f158"; -@fa-var-ruble: "\f158"; -@fa-var-rupee: "\f156"; -@fa-var-safari: "\f267"; -@fa-var-save: "\f0c7"; -@fa-var-scissors: "\f0c4"; -@fa-var-scribd: "\f28a"; -@fa-var-search: "\f002"; -@fa-var-search-minus: "\f010"; -@fa-var-search-plus: "\f00e"; -@fa-var-sellsy: "\f213"; -@fa-var-send: "\f1d8"; -@fa-var-send-o: "\f1d9"; -@fa-var-server: "\f233"; -@fa-var-share: "\f064"; -@fa-var-share-alt: "\f1e0"; -@fa-var-share-alt-square: "\f1e1"; -@fa-var-share-square: "\f14d"; -@fa-var-share-square-o: "\f045"; -@fa-var-shekel: "\f20b"; -@fa-var-sheqel: "\f20b"; -@fa-var-shield: "\f132"; -@fa-var-ship: "\f21a"; -@fa-var-shirtsinbulk: "\f214"; -@fa-var-shopping-bag: "\f290"; -@fa-var-shopping-basket: "\f291"; -@fa-var-shopping-cart: "\f07a"; -@fa-var-sign-in: "\f090"; -@fa-var-sign-language: "\f2a7"; -@fa-var-sign-out: "\f08b"; -@fa-var-signal: "\f012"; -@fa-var-signing: "\f2a7"; -@fa-var-simplybuilt: "\f215"; -@fa-var-sitemap: "\f0e8"; -@fa-var-skyatlas: "\f216"; -@fa-var-skype: "\f17e"; -@fa-var-slack: "\f198"; -@fa-var-sliders: "\f1de"; -@fa-var-slideshare: "\f1e7"; -@fa-var-smile-o: "\f118"; -@fa-var-snapchat: "\f2ab"; -@fa-var-snapchat-ghost: "\f2ac"; -@fa-var-snapchat-square: "\f2ad"; -@fa-var-soccer-ball-o: "\f1e3"; -@fa-var-sort: "\f0dc"; -@fa-var-sort-alpha-asc: "\f15d"; -@fa-var-sort-alpha-desc: "\f15e"; -@fa-var-sort-amount-asc: "\f160"; -@fa-var-sort-amount-desc: "\f161"; -@fa-var-sort-asc: "\f0de"; -@fa-var-sort-desc: "\f0dd"; -@fa-var-sort-down: "\f0dd"; -@fa-var-sort-numeric-asc: "\f162"; -@fa-var-sort-numeric-desc: "\f163"; -@fa-var-sort-up: "\f0de"; -@fa-var-soundcloud: "\f1be"; -@fa-var-space-shuttle: "\f197"; -@fa-var-spinner: "\f110"; -@fa-var-spoon: "\f1b1"; -@fa-var-spotify: "\f1bc"; -@fa-var-square: "\f0c8"; -@fa-var-square-o: "\f096"; -@fa-var-stack-exchange: "\f18d"; -@fa-var-stack-overflow: "\f16c"; -@fa-var-star: "\f005"; -@fa-var-star-half: "\f089"; -@fa-var-star-half-empty: "\f123"; -@fa-var-star-half-full: "\f123"; -@fa-var-star-half-o: "\f123"; -@fa-var-star-o: "\f006"; -@fa-var-steam: "\f1b6"; -@fa-var-steam-square: "\f1b7"; -@fa-var-step-backward: "\f048"; -@fa-var-step-forward: "\f051"; -@fa-var-stethoscope: "\f0f1"; -@fa-var-sticky-note: "\f249"; -@fa-var-sticky-note-o: "\f24a"; -@fa-var-stop: "\f04d"; -@fa-var-stop-circle: "\f28d"; -@fa-var-stop-circle-o: "\f28e"; -@fa-var-street-view: "\f21d"; -@fa-var-strikethrough: "\f0cc"; -@fa-var-stumbleupon: "\f1a4"; -@fa-var-stumbleupon-circle: "\f1a3"; -@fa-var-subscript: "\f12c"; -@fa-var-subway: "\f239"; -@fa-var-suitcase: "\f0f2"; -@fa-var-sun-o: "\f185"; -@fa-var-superscript: "\f12b"; -@fa-var-support: "\f1cd"; -@fa-var-table: "\f0ce"; -@fa-var-tablet: "\f10a"; -@fa-var-tachometer: "\f0e4"; -@fa-var-tag: "\f02b"; -@fa-var-tags: "\f02c"; -@fa-var-tasks: "\f0ae"; -@fa-var-taxi: "\f1ba"; -@fa-var-television: "\f26c"; -@fa-var-tencent-weibo: "\f1d5"; -@fa-var-terminal: "\f120"; -@fa-var-text-height: "\f034"; -@fa-var-text-width: "\f035"; -@fa-var-th: "\f00a"; -@fa-var-th-large: "\f009"; -@fa-var-th-list: "\f00b"; -@fa-var-thumb-tack: "\f08d"; -@fa-var-thumbs-down: "\f165"; -@fa-var-thumbs-o-down: "\f088"; -@fa-var-thumbs-o-up: "\f087"; -@fa-var-thumbs-up: "\f164"; -@fa-var-ticket: "\f145"; -@fa-var-times: "\f00d"; -@fa-var-times-circle: "\f057"; -@fa-var-times-circle-o: "\f05c"; -@fa-var-tint: "\f043"; -@fa-var-toggle-down: "\f150"; -@fa-var-toggle-left: "\f191"; -@fa-var-toggle-off: "\f204"; -@fa-var-toggle-on: "\f205"; -@fa-var-toggle-right: "\f152"; -@fa-var-toggle-up: "\f151"; -@fa-var-trademark: "\f25c"; -@fa-var-train: "\f238"; -@fa-var-transgender: "\f224"; -@fa-var-transgender-alt: "\f225"; -@fa-var-trash: "\f1f8"; -@fa-var-trash-o: "\f014"; -@fa-var-tree: "\f1bb"; -@fa-var-trello: "\f181"; -@fa-var-tripadvisor: "\f262"; -@fa-var-trophy: "\f091"; -@fa-var-truck: "\f0d1"; -@fa-var-try: "\f195"; -@fa-var-tty: "\f1e4"; -@fa-var-tumblr: "\f173"; -@fa-var-tumblr-square: "\f174"; -@fa-var-turkish-lira: "\f195"; -@fa-var-tv: "\f26c"; -@fa-var-twitch: "\f1e8"; -@fa-var-twitter: "\f099"; -@fa-var-twitter-square: "\f081"; -@fa-var-umbrella: "\f0e9"; -@fa-var-underline: "\f0cd"; -@fa-var-undo: "\f0e2"; -@fa-var-universal-access: "\f29a"; -@fa-var-university: "\f19c"; -@fa-var-unlink: "\f127"; -@fa-var-unlock: "\f09c"; -@fa-var-unlock-alt: "\f13e"; -@fa-var-unsorted: "\f0dc"; -@fa-var-upload: "\f093"; -@fa-var-usb: "\f287"; -@fa-var-usd: "\f155"; -@fa-var-user: "\f007"; -@fa-var-user-md: "\f0f0"; -@fa-var-user-plus: "\f234"; -@fa-var-user-secret: "\f21b"; -@fa-var-user-times: "\f235"; -@fa-var-users: "\f0c0"; -@fa-var-venus: "\f221"; -@fa-var-venus-double: "\f226"; -@fa-var-venus-mars: "\f228"; -@fa-var-viacoin: "\f237"; -@fa-var-viadeo: "\f2a9"; -@fa-var-viadeo-square: "\f2aa"; -@fa-var-video-camera: "\f03d"; -@fa-var-vimeo: "\f27d"; -@fa-var-vimeo-square: "\f194"; -@fa-var-vine: "\f1ca"; -@fa-var-vk: "\f189"; -@fa-var-volume-control-phone: "\f2a0"; -@fa-var-volume-down: "\f027"; -@fa-var-volume-off: "\f026"; -@fa-var-volume-up: "\f028"; -@fa-var-warning: "\f071"; -@fa-var-wechat: "\f1d7"; -@fa-var-weibo: "\f18a"; -@fa-var-weixin: "\f1d7"; -@fa-var-whatsapp: "\f232"; -@fa-var-wheelchair: "\f193"; -@fa-var-wheelchair-alt: "\f29b"; -@fa-var-wifi: "\f1eb"; -@fa-var-wikipedia-w: "\f266"; -@fa-var-windows: "\f17a"; -@fa-var-won: "\f159"; -@fa-var-wordpress: "\f19a"; -@fa-var-wpbeginner: "\f297"; -@fa-var-wpforms: "\f298"; -@fa-var-wrench: "\f0ad"; -@fa-var-xing: "\f168"; -@fa-var-xing-square: "\f169"; -@fa-var-y-combinator: "\f23b"; -@fa-var-y-combinator-square: "\f1d4"; -@fa-var-yahoo: "\f19e"; -@fa-var-yc: "\f23b"; -@fa-var-yc-square: "\f1d4"; -@fa-var-yelp: "\f1e9"; -@fa-var-yen: "\f157"; -@fa-var-youtube: "\f167"; -@fa-var-youtube-play: "\f16a"; -@fa-var-youtube-square: "\f166"; - diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_animated.scss b/iam-login-service/src/main/webapp/resources/font-awesome/scss/_animated.scss deleted file mode 100644 index 8a020dbff..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_animated.scss +++ /dev/null @@ -1,34 +0,0 @@ -// Spinning Icons -// -------------------------- - -.#{$fa-css-prefix}-spin { - -webkit-animation: fa-spin 2s infinite linear; - animation: fa-spin 2s infinite linear; -} - -.#{$fa-css-prefix}-pulse { - -webkit-animation: fa-spin 1s infinite steps(8); - animation: fa-spin 1s infinite steps(8); -} - -@-webkit-keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} - -@keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_bordered-pulled.scss b/iam-login-service/src/main/webapp/resources/font-awesome/scss/_bordered-pulled.scss deleted file mode 100644 index d4b85a02f..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_bordered-pulled.scss +++ /dev/null @@ -1,25 +0,0 @@ -// Bordered & Pulled -// ------------------------- - -.#{$fa-css-prefix}-border { - padding: .2em .25em .15em; - border: solid .08em $fa-border-color; - border-radius: .1em; -} - -.#{$fa-css-prefix}-pull-left { float: left; } -.#{$fa-css-prefix}-pull-right { float: right; } - -.#{$fa-css-prefix} { - &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } - &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } -} - -/* Deprecated as of 4.4.0 */ -.pull-right { float: right; } -.pull-left { float: left; } - -.#{$fa-css-prefix} { - &.pull-left { margin-right: .3em; } - &.pull-right { margin-left: .3em; } -} diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_core.scss b/iam-login-service/src/main/webapp/resources/font-awesome/scss/_core.scss deleted file mode 100644 index 7425ef85f..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_core.scss +++ /dev/null @@ -1,12 +0,0 @@ -// Base Class Definition -// ------------------------- - -.#{$fa-css-prefix} { - display: inline-block; - font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration - font-size: inherit; // can't have font-size inherit on line above, so need to override - text-rendering: auto; // optimizelegibility throws things off #1094 - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -} diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_fixed-width.scss b/iam-login-service/src/main/webapp/resources/font-awesome/scss/_fixed-width.scss deleted file mode 100644 index b221c9813..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_fixed-width.scss +++ /dev/null @@ -1,6 +0,0 @@ -// Fixed Width Icons -// ------------------------- -.#{$fa-css-prefix}-fw { - width: (18em / 14); - text-align: center; -} diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_icons.scss b/iam-login-service/src/main/webapp/resources/font-awesome/scss/_icons.scss deleted file mode 100644 index b64017aef..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_icons.scss +++ /dev/null @@ -1,724 +0,0 @@ -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ - -.#{$fa-css-prefix}-glass:before { content: $fa-var-glass; } -.#{$fa-css-prefix}-music:before { content: $fa-var-music; } -.#{$fa-css-prefix}-search:before { content: $fa-var-search; } -.#{$fa-css-prefix}-envelope-o:before { content: $fa-var-envelope-o; } -.#{$fa-css-prefix}-heart:before { content: $fa-var-heart; } -.#{$fa-css-prefix}-star:before { content: $fa-var-star; } -.#{$fa-css-prefix}-star-o:before { content: $fa-var-star-o; } -.#{$fa-css-prefix}-user:before { content: $fa-var-user; } -.#{$fa-css-prefix}-film:before { content: $fa-var-film; } -.#{$fa-css-prefix}-th-large:before { content: $fa-var-th-large; } -.#{$fa-css-prefix}-th:before { content: $fa-var-th; } -.#{$fa-css-prefix}-th-list:before { content: $fa-var-th-list; } -.#{$fa-css-prefix}-check:before { content: $fa-var-check; } -.#{$fa-css-prefix}-remove:before, -.#{$fa-css-prefix}-close:before, -.#{$fa-css-prefix}-times:before { content: $fa-var-times; } -.#{$fa-css-prefix}-search-plus:before { content: $fa-var-search-plus; } -.#{$fa-css-prefix}-search-minus:before { content: $fa-var-search-minus; } -.#{$fa-css-prefix}-power-off:before { content: $fa-var-power-off; } -.#{$fa-css-prefix}-signal:before { content: $fa-var-signal; } -.#{$fa-css-prefix}-gear:before, -.#{$fa-css-prefix}-cog:before { content: $fa-var-cog; } -.#{$fa-css-prefix}-trash-o:before { content: $fa-var-trash-o; } -.#{$fa-css-prefix}-home:before { content: $fa-var-home; } -.#{$fa-css-prefix}-file-o:before { content: $fa-var-file-o; } -.#{$fa-css-prefix}-clock-o:before { content: $fa-var-clock-o; } -.#{$fa-css-prefix}-road:before { content: $fa-var-road; } -.#{$fa-css-prefix}-download:before { content: $fa-var-download; } -.#{$fa-css-prefix}-arrow-circle-o-down:before { content: $fa-var-arrow-circle-o-down; } -.#{$fa-css-prefix}-arrow-circle-o-up:before { content: $fa-var-arrow-circle-o-up; } -.#{$fa-css-prefix}-inbox:before { content: $fa-var-inbox; } -.#{$fa-css-prefix}-play-circle-o:before { content: $fa-var-play-circle-o; } -.#{$fa-css-prefix}-rotate-right:before, -.#{$fa-css-prefix}-repeat:before { content: $fa-var-repeat; } -.#{$fa-css-prefix}-refresh:before { content: $fa-var-refresh; } -.#{$fa-css-prefix}-list-alt:before { content: $fa-var-list-alt; } -.#{$fa-css-prefix}-lock:before { content: $fa-var-lock; } -.#{$fa-css-prefix}-flag:before { content: $fa-var-flag; } -.#{$fa-css-prefix}-headphones:before { content: $fa-var-headphones; } -.#{$fa-css-prefix}-volume-off:before { content: $fa-var-volume-off; } -.#{$fa-css-prefix}-volume-down:before { content: $fa-var-volume-down; } -.#{$fa-css-prefix}-volume-up:before { content: $fa-var-volume-up; } -.#{$fa-css-prefix}-qrcode:before { content: $fa-var-qrcode; } -.#{$fa-css-prefix}-barcode:before { content: $fa-var-barcode; } -.#{$fa-css-prefix}-tag:before { content: $fa-var-tag; } -.#{$fa-css-prefix}-tags:before { content: $fa-var-tags; } -.#{$fa-css-prefix}-book:before { content: $fa-var-book; } -.#{$fa-css-prefix}-bookmark:before { content: $fa-var-bookmark; } -.#{$fa-css-prefix}-print:before { content: $fa-var-print; } -.#{$fa-css-prefix}-camera:before { content: $fa-var-camera; } -.#{$fa-css-prefix}-font:before { content: $fa-var-font; } -.#{$fa-css-prefix}-bold:before { content: $fa-var-bold; } -.#{$fa-css-prefix}-italic:before { content: $fa-var-italic; } -.#{$fa-css-prefix}-text-height:before { content: $fa-var-text-height; } -.#{$fa-css-prefix}-text-width:before { content: $fa-var-text-width; } -.#{$fa-css-prefix}-align-left:before { content: $fa-var-align-left; } -.#{$fa-css-prefix}-align-center:before { content: $fa-var-align-center; } -.#{$fa-css-prefix}-align-right:before { content: $fa-var-align-right; } -.#{$fa-css-prefix}-align-justify:before { content: $fa-var-align-justify; } -.#{$fa-css-prefix}-list:before { content: $fa-var-list; } -.#{$fa-css-prefix}-dedent:before, -.#{$fa-css-prefix}-outdent:before { content: $fa-var-outdent; } -.#{$fa-css-prefix}-indent:before { content: $fa-var-indent; } -.#{$fa-css-prefix}-video-camera:before { content: $fa-var-video-camera; } -.#{$fa-css-prefix}-photo:before, -.#{$fa-css-prefix}-image:before, -.#{$fa-css-prefix}-picture-o:before { content: $fa-var-picture-o; } -.#{$fa-css-prefix}-pencil:before { content: $fa-var-pencil; } -.#{$fa-css-prefix}-map-marker:before { content: $fa-var-map-marker; } -.#{$fa-css-prefix}-adjust:before { content: $fa-var-adjust; } -.#{$fa-css-prefix}-tint:before { content: $fa-var-tint; } -.#{$fa-css-prefix}-edit:before, -.#{$fa-css-prefix}-pencil-square-o:before { content: $fa-var-pencil-square-o; } -.#{$fa-css-prefix}-share-square-o:before { content: $fa-var-share-square-o; } -.#{$fa-css-prefix}-check-square-o:before { content: $fa-var-check-square-o; } -.#{$fa-css-prefix}-arrows:before { content: $fa-var-arrows; } -.#{$fa-css-prefix}-step-backward:before { content: $fa-var-step-backward; } -.#{$fa-css-prefix}-fast-backward:before { content: $fa-var-fast-backward; } -.#{$fa-css-prefix}-backward:before { content: $fa-var-backward; } -.#{$fa-css-prefix}-play:before { content: $fa-var-play; } -.#{$fa-css-prefix}-pause:before { content: $fa-var-pause; } -.#{$fa-css-prefix}-stop:before { content: $fa-var-stop; } -.#{$fa-css-prefix}-forward:before { content: $fa-var-forward; } -.#{$fa-css-prefix}-fast-forward:before { content: $fa-var-fast-forward; } -.#{$fa-css-prefix}-step-forward:before { content: $fa-var-step-forward; } -.#{$fa-css-prefix}-eject:before { content: $fa-var-eject; } -.#{$fa-css-prefix}-chevron-left:before { content: $fa-var-chevron-left; } -.#{$fa-css-prefix}-chevron-right:before { content: $fa-var-chevron-right; } -.#{$fa-css-prefix}-plus-circle:before { content: $fa-var-plus-circle; } -.#{$fa-css-prefix}-minus-circle:before { content: $fa-var-minus-circle; } -.#{$fa-css-prefix}-times-circle:before { content: $fa-var-times-circle; } -.#{$fa-css-prefix}-check-circle:before { content: $fa-var-check-circle; } -.#{$fa-css-prefix}-question-circle:before { content: $fa-var-question-circle; } -.#{$fa-css-prefix}-info-circle:before { content: $fa-var-info-circle; } -.#{$fa-css-prefix}-crosshairs:before { content: $fa-var-crosshairs; } -.#{$fa-css-prefix}-times-circle-o:before { content: $fa-var-times-circle-o; } -.#{$fa-css-prefix}-check-circle-o:before { content: $fa-var-check-circle-o; } -.#{$fa-css-prefix}-ban:before { content: $fa-var-ban; } -.#{$fa-css-prefix}-arrow-left:before { content: $fa-var-arrow-left; } -.#{$fa-css-prefix}-arrow-right:before { content: $fa-var-arrow-right; } -.#{$fa-css-prefix}-arrow-up:before { content: $fa-var-arrow-up; } -.#{$fa-css-prefix}-arrow-down:before { content: $fa-var-arrow-down; } -.#{$fa-css-prefix}-mail-forward:before, -.#{$fa-css-prefix}-share:before { content: $fa-var-share; } -.#{$fa-css-prefix}-expand:before { content: $fa-var-expand; } -.#{$fa-css-prefix}-compress:before { content: $fa-var-compress; } -.#{$fa-css-prefix}-plus:before { content: $fa-var-plus; } -.#{$fa-css-prefix}-minus:before { content: $fa-var-minus; } -.#{$fa-css-prefix}-asterisk:before { content: $fa-var-asterisk; } -.#{$fa-css-prefix}-exclamation-circle:before { content: $fa-var-exclamation-circle; } -.#{$fa-css-prefix}-gift:before { content: $fa-var-gift; } -.#{$fa-css-prefix}-leaf:before { content: $fa-var-leaf; } -.#{$fa-css-prefix}-fire:before { content: $fa-var-fire; } -.#{$fa-css-prefix}-eye:before { content: $fa-var-eye; } -.#{$fa-css-prefix}-eye-slash:before { content: $fa-var-eye-slash; } -.#{$fa-css-prefix}-warning:before, -.#{$fa-css-prefix}-exclamation-triangle:before { content: $fa-var-exclamation-triangle; } -.#{$fa-css-prefix}-plane:before { content: $fa-var-plane; } -.#{$fa-css-prefix}-calendar:before { content: $fa-var-calendar; } -.#{$fa-css-prefix}-random:before { content: $fa-var-random; } -.#{$fa-css-prefix}-comment:before { content: $fa-var-comment; } -.#{$fa-css-prefix}-magnet:before { content: $fa-var-magnet; } -.#{$fa-css-prefix}-chevron-up:before { content: $fa-var-chevron-up; } -.#{$fa-css-prefix}-chevron-down:before { content: $fa-var-chevron-down; } -.#{$fa-css-prefix}-retweet:before { content: $fa-var-retweet; } -.#{$fa-css-prefix}-shopping-cart:before { content: $fa-var-shopping-cart; } -.#{$fa-css-prefix}-folder:before { content: $fa-var-folder; } -.#{$fa-css-prefix}-folder-open:before { content: $fa-var-folder-open; } -.#{$fa-css-prefix}-arrows-v:before { content: $fa-var-arrows-v; } -.#{$fa-css-prefix}-arrows-h:before { content: $fa-var-arrows-h; } -.#{$fa-css-prefix}-bar-chart-o:before, -.#{$fa-css-prefix}-bar-chart:before { content: $fa-var-bar-chart; } -.#{$fa-css-prefix}-twitter-square:before { content: $fa-var-twitter-square; } -.#{$fa-css-prefix}-facebook-square:before { content: $fa-var-facebook-square; } -.#{$fa-css-prefix}-camera-retro:before { content: $fa-var-camera-retro; } -.#{$fa-css-prefix}-key:before { content: $fa-var-key; } -.#{$fa-css-prefix}-gears:before, -.#{$fa-css-prefix}-cogs:before { content: $fa-var-cogs; } -.#{$fa-css-prefix}-comments:before { content: $fa-var-comments; } -.#{$fa-css-prefix}-thumbs-o-up:before { content: $fa-var-thumbs-o-up; } -.#{$fa-css-prefix}-thumbs-o-down:before { content: $fa-var-thumbs-o-down; } -.#{$fa-css-prefix}-star-half:before { content: $fa-var-star-half; } -.#{$fa-css-prefix}-heart-o:before { content: $fa-var-heart-o; } -.#{$fa-css-prefix}-sign-out:before { content: $fa-var-sign-out; } -.#{$fa-css-prefix}-linkedin-square:before { content: $fa-var-linkedin-square; } -.#{$fa-css-prefix}-thumb-tack:before { content: $fa-var-thumb-tack; } -.#{$fa-css-prefix}-external-link:before { content: $fa-var-external-link; } -.#{$fa-css-prefix}-sign-in:before { content: $fa-var-sign-in; } -.#{$fa-css-prefix}-trophy:before { content: $fa-var-trophy; } -.#{$fa-css-prefix}-github-square:before { content: $fa-var-github-square; } -.#{$fa-css-prefix}-upload:before { content: $fa-var-upload; } -.#{$fa-css-prefix}-lemon-o:before { content: $fa-var-lemon-o; } -.#{$fa-css-prefix}-phone:before { content: $fa-var-phone; } -.#{$fa-css-prefix}-square-o:before { content: $fa-var-square-o; } -.#{$fa-css-prefix}-bookmark-o:before { content: $fa-var-bookmark-o; } -.#{$fa-css-prefix}-phone-square:before { content: $fa-var-phone-square; } -.#{$fa-css-prefix}-twitter:before { content: $fa-var-twitter; } -.#{$fa-css-prefix}-facebook-f:before, -.#{$fa-css-prefix}-facebook:before { content: $fa-var-facebook; } -.#{$fa-css-prefix}-github:before { content: $fa-var-github; } -.#{$fa-css-prefix}-unlock:before { content: $fa-var-unlock; } -.#{$fa-css-prefix}-credit-card:before { content: $fa-var-credit-card; } -.#{$fa-css-prefix}-feed:before, -.#{$fa-css-prefix}-rss:before { content: $fa-var-rss; } -.#{$fa-css-prefix}-hdd-o:before { content: $fa-var-hdd-o; } -.#{$fa-css-prefix}-bullhorn:before { content: $fa-var-bullhorn; } -.#{$fa-css-prefix}-bell:before { content: $fa-var-bell; } -.#{$fa-css-prefix}-certificate:before { content: $fa-var-certificate; } -.#{$fa-css-prefix}-hand-o-right:before { content: $fa-var-hand-o-right; } -.#{$fa-css-prefix}-hand-o-left:before { content: $fa-var-hand-o-left; } -.#{$fa-css-prefix}-hand-o-up:before { content: $fa-var-hand-o-up; } -.#{$fa-css-prefix}-hand-o-down:before { content: $fa-var-hand-o-down; } -.#{$fa-css-prefix}-arrow-circle-left:before { content: $fa-var-arrow-circle-left; } -.#{$fa-css-prefix}-arrow-circle-right:before { content: $fa-var-arrow-circle-right; } -.#{$fa-css-prefix}-arrow-circle-up:before { content: $fa-var-arrow-circle-up; } -.#{$fa-css-prefix}-arrow-circle-down:before { content: $fa-var-arrow-circle-down; } -.#{$fa-css-prefix}-globe:before { content: $fa-var-globe; } -.#{$fa-css-prefix}-wrench:before { content: $fa-var-wrench; } -.#{$fa-css-prefix}-tasks:before { content: $fa-var-tasks; } -.#{$fa-css-prefix}-filter:before { content: $fa-var-filter; } -.#{$fa-css-prefix}-briefcase:before { content: $fa-var-briefcase; } -.#{$fa-css-prefix}-arrows-alt:before { content: $fa-var-arrows-alt; } -.#{$fa-css-prefix}-group:before, -.#{$fa-css-prefix}-users:before { content: $fa-var-users; } -.#{$fa-css-prefix}-chain:before, -.#{$fa-css-prefix}-link:before { content: $fa-var-link; } -.#{$fa-css-prefix}-cloud:before { content: $fa-var-cloud; } -.#{$fa-css-prefix}-flask:before { content: $fa-var-flask; } -.#{$fa-css-prefix}-cut:before, -.#{$fa-css-prefix}-scissors:before { content: $fa-var-scissors; } -.#{$fa-css-prefix}-copy:before, -.#{$fa-css-prefix}-files-o:before { content: $fa-var-files-o; } -.#{$fa-css-prefix}-paperclip:before { content: $fa-var-paperclip; } -.#{$fa-css-prefix}-save:before, -.#{$fa-css-prefix}-floppy-o:before { content: $fa-var-floppy-o; } -.#{$fa-css-prefix}-square:before { content: $fa-var-square; } -.#{$fa-css-prefix}-navicon:before, -.#{$fa-css-prefix}-reorder:before, -.#{$fa-css-prefix}-bars:before { content: $fa-var-bars; } -.#{$fa-css-prefix}-list-ul:before { content: $fa-var-list-ul; } -.#{$fa-css-prefix}-list-ol:before { content: $fa-var-list-ol; } -.#{$fa-css-prefix}-strikethrough:before { content: $fa-var-strikethrough; } -.#{$fa-css-prefix}-underline:before { content: $fa-var-underline; } -.#{$fa-css-prefix}-table:before { content: $fa-var-table; } -.#{$fa-css-prefix}-magic:before { content: $fa-var-magic; } -.#{$fa-css-prefix}-truck:before { content: $fa-var-truck; } -.#{$fa-css-prefix}-pinterest:before { content: $fa-var-pinterest; } -.#{$fa-css-prefix}-pinterest-square:before { content: $fa-var-pinterest-square; } -.#{$fa-css-prefix}-google-plus-square:before { content: $fa-var-google-plus-square; } -.#{$fa-css-prefix}-google-plus:before { content: $fa-var-google-plus; } -.#{$fa-css-prefix}-money:before { content: $fa-var-money; } -.#{$fa-css-prefix}-caret-down:before { content: $fa-var-caret-down; } -.#{$fa-css-prefix}-caret-up:before { content: $fa-var-caret-up; } -.#{$fa-css-prefix}-caret-left:before { content: $fa-var-caret-left; } -.#{$fa-css-prefix}-caret-right:before { content: $fa-var-caret-right; } -.#{$fa-css-prefix}-columns:before { content: $fa-var-columns; } -.#{$fa-css-prefix}-unsorted:before, -.#{$fa-css-prefix}-sort:before { content: $fa-var-sort; } -.#{$fa-css-prefix}-sort-down:before, -.#{$fa-css-prefix}-sort-desc:before { content: $fa-var-sort-desc; } -.#{$fa-css-prefix}-sort-up:before, -.#{$fa-css-prefix}-sort-asc:before { content: $fa-var-sort-asc; } -.#{$fa-css-prefix}-envelope:before { content: $fa-var-envelope; } -.#{$fa-css-prefix}-linkedin:before { content: $fa-var-linkedin; } -.#{$fa-css-prefix}-rotate-left:before, -.#{$fa-css-prefix}-undo:before { content: $fa-var-undo; } -.#{$fa-css-prefix}-legal:before, -.#{$fa-css-prefix}-gavel:before { content: $fa-var-gavel; } -.#{$fa-css-prefix}-dashboard:before, -.#{$fa-css-prefix}-tachometer:before { content: $fa-var-tachometer; } -.#{$fa-css-prefix}-comment-o:before { content: $fa-var-comment-o; } -.#{$fa-css-prefix}-comments-o:before { content: $fa-var-comments-o; } -.#{$fa-css-prefix}-flash:before, -.#{$fa-css-prefix}-bolt:before { content: $fa-var-bolt; } -.#{$fa-css-prefix}-sitemap:before { content: $fa-var-sitemap; } -.#{$fa-css-prefix}-umbrella:before { content: $fa-var-umbrella; } -.#{$fa-css-prefix}-paste:before, -.#{$fa-css-prefix}-clipboard:before { content: $fa-var-clipboard; } -.#{$fa-css-prefix}-lightbulb-o:before { content: $fa-var-lightbulb-o; } -.#{$fa-css-prefix}-exchange:before { content: $fa-var-exchange; } -.#{$fa-css-prefix}-cloud-download:before { content: $fa-var-cloud-download; } -.#{$fa-css-prefix}-cloud-upload:before { content: $fa-var-cloud-upload; } -.#{$fa-css-prefix}-user-md:before { content: $fa-var-user-md; } -.#{$fa-css-prefix}-stethoscope:before { content: $fa-var-stethoscope; } -.#{$fa-css-prefix}-suitcase:before { content: $fa-var-suitcase; } -.#{$fa-css-prefix}-bell-o:before { content: $fa-var-bell-o; } -.#{$fa-css-prefix}-coffee:before { content: $fa-var-coffee; } -.#{$fa-css-prefix}-cutlery:before { content: $fa-var-cutlery; } -.#{$fa-css-prefix}-file-text-o:before { content: $fa-var-file-text-o; } -.#{$fa-css-prefix}-building-o:before { content: $fa-var-building-o; } -.#{$fa-css-prefix}-hospital-o:before { content: $fa-var-hospital-o; } -.#{$fa-css-prefix}-ambulance:before { content: $fa-var-ambulance; } -.#{$fa-css-prefix}-medkit:before { content: $fa-var-medkit; } -.#{$fa-css-prefix}-fighter-jet:before { content: $fa-var-fighter-jet; } -.#{$fa-css-prefix}-beer:before { content: $fa-var-beer; } -.#{$fa-css-prefix}-h-square:before { content: $fa-var-h-square; } -.#{$fa-css-prefix}-plus-square:before { content: $fa-var-plus-square; } -.#{$fa-css-prefix}-angle-double-left:before { content: $fa-var-angle-double-left; } -.#{$fa-css-prefix}-angle-double-right:before { content: $fa-var-angle-double-right; } -.#{$fa-css-prefix}-angle-double-up:before { content: $fa-var-angle-double-up; } -.#{$fa-css-prefix}-angle-double-down:before { content: $fa-var-angle-double-down; } -.#{$fa-css-prefix}-angle-left:before { content: $fa-var-angle-left; } -.#{$fa-css-prefix}-angle-right:before { content: $fa-var-angle-right; } -.#{$fa-css-prefix}-angle-up:before { content: $fa-var-angle-up; } -.#{$fa-css-prefix}-angle-down:before { content: $fa-var-angle-down; } -.#{$fa-css-prefix}-desktop:before { content: $fa-var-desktop; } -.#{$fa-css-prefix}-laptop:before { content: $fa-var-laptop; } -.#{$fa-css-prefix}-tablet:before { content: $fa-var-tablet; } -.#{$fa-css-prefix}-mobile-phone:before, -.#{$fa-css-prefix}-mobile:before { content: $fa-var-mobile; } -.#{$fa-css-prefix}-circle-o:before { content: $fa-var-circle-o; } -.#{$fa-css-prefix}-quote-left:before { content: $fa-var-quote-left; } -.#{$fa-css-prefix}-quote-right:before { content: $fa-var-quote-right; } -.#{$fa-css-prefix}-spinner:before { content: $fa-var-spinner; } -.#{$fa-css-prefix}-circle:before { content: $fa-var-circle; } -.#{$fa-css-prefix}-mail-reply:before, -.#{$fa-css-prefix}-reply:before { content: $fa-var-reply; } -.#{$fa-css-prefix}-github-alt:before { content: $fa-var-github-alt; } -.#{$fa-css-prefix}-folder-o:before { content: $fa-var-folder-o; } -.#{$fa-css-prefix}-folder-open-o:before { content: $fa-var-folder-open-o; } -.#{$fa-css-prefix}-smile-o:before { content: $fa-var-smile-o; } -.#{$fa-css-prefix}-frown-o:before { content: $fa-var-frown-o; } -.#{$fa-css-prefix}-meh-o:before { content: $fa-var-meh-o; } -.#{$fa-css-prefix}-gamepad:before { content: $fa-var-gamepad; } -.#{$fa-css-prefix}-keyboard-o:before { content: $fa-var-keyboard-o; } -.#{$fa-css-prefix}-flag-o:before { content: $fa-var-flag-o; } -.#{$fa-css-prefix}-flag-checkered:before { content: $fa-var-flag-checkered; } -.#{$fa-css-prefix}-terminal:before { content: $fa-var-terminal; } -.#{$fa-css-prefix}-code:before { content: $fa-var-code; } -.#{$fa-css-prefix}-mail-reply-all:before, -.#{$fa-css-prefix}-reply-all:before { content: $fa-var-reply-all; } -.#{$fa-css-prefix}-star-half-empty:before, -.#{$fa-css-prefix}-star-half-full:before, -.#{$fa-css-prefix}-star-half-o:before { content: $fa-var-star-half-o; } -.#{$fa-css-prefix}-location-arrow:before { content: $fa-var-location-arrow; } -.#{$fa-css-prefix}-crop:before { content: $fa-var-crop; } -.#{$fa-css-prefix}-code-fork:before { content: $fa-var-code-fork; } -.#{$fa-css-prefix}-unlink:before, -.#{$fa-css-prefix}-chain-broken:before { content: $fa-var-chain-broken; } -.#{$fa-css-prefix}-question:before { content: $fa-var-question; } -.#{$fa-css-prefix}-info:before { content: $fa-var-info; } -.#{$fa-css-prefix}-exclamation:before { content: $fa-var-exclamation; } -.#{$fa-css-prefix}-superscript:before { content: $fa-var-superscript; } -.#{$fa-css-prefix}-subscript:before { content: $fa-var-subscript; } -.#{$fa-css-prefix}-eraser:before { content: $fa-var-eraser; } -.#{$fa-css-prefix}-puzzle-piece:before { content: $fa-var-puzzle-piece; } -.#{$fa-css-prefix}-microphone:before { content: $fa-var-microphone; } -.#{$fa-css-prefix}-microphone-slash:before { content: $fa-var-microphone-slash; } -.#{$fa-css-prefix}-shield:before { content: $fa-var-shield; } -.#{$fa-css-prefix}-calendar-o:before { content: $fa-var-calendar-o; } -.#{$fa-css-prefix}-fire-extinguisher:before { content: $fa-var-fire-extinguisher; } -.#{$fa-css-prefix}-rocket:before { content: $fa-var-rocket; } -.#{$fa-css-prefix}-maxcdn:before { content: $fa-var-maxcdn; } -.#{$fa-css-prefix}-chevron-circle-left:before { content: $fa-var-chevron-circle-left; } -.#{$fa-css-prefix}-chevron-circle-right:before { content: $fa-var-chevron-circle-right; } -.#{$fa-css-prefix}-chevron-circle-up:before { content: $fa-var-chevron-circle-up; } -.#{$fa-css-prefix}-chevron-circle-down:before { content: $fa-var-chevron-circle-down; } -.#{$fa-css-prefix}-html5:before { content: $fa-var-html5; } -.#{$fa-css-prefix}-css3:before { content: $fa-var-css3; } -.#{$fa-css-prefix}-anchor:before { content: $fa-var-anchor; } -.#{$fa-css-prefix}-unlock-alt:before { content: $fa-var-unlock-alt; } -.#{$fa-css-prefix}-bullseye:before { content: $fa-var-bullseye; } -.#{$fa-css-prefix}-ellipsis-h:before { content: $fa-var-ellipsis-h; } -.#{$fa-css-prefix}-ellipsis-v:before { content: $fa-var-ellipsis-v; } -.#{$fa-css-prefix}-rss-square:before { content: $fa-var-rss-square; } -.#{$fa-css-prefix}-play-circle:before { content: $fa-var-play-circle; } -.#{$fa-css-prefix}-ticket:before { content: $fa-var-ticket; } -.#{$fa-css-prefix}-minus-square:before { content: $fa-var-minus-square; } -.#{$fa-css-prefix}-minus-square-o:before { content: $fa-var-minus-square-o; } -.#{$fa-css-prefix}-level-up:before { content: $fa-var-level-up; } -.#{$fa-css-prefix}-level-down:before { content: $fa-var-level-down; } -.#{$fa-css-prefix}-check-square:before { content: $fa-var-check-square; } -.#{$fa-css-prefix}-pencil-square:before { content: $fa-var-pencil-square; } -.#{$fa-css-prefix}-external-link-square:before { content: $fa-var-external-link-square; } -.#{$fa-css-prefix}-share-square:before { content: $fa-var-share-square; } -.#{$fa-css-prefix}-compass:before { content: $fa-var-compass; } -.#{$fa-css-prefix}-toggle-down:before, -.#{$fa-css-prefix}-caret-square-o-down:before { content: $fa-var-caret-square-o-down; } -.#{$fa-css-prefix}-toggle-up:before, -.#{$fa-css-prefix}-caret-square-o-up:before { content: $fa-var-caret-square-o-up; } -.#{$fa-css-prefix}-toggle-right:before, -.#{$fa-css-prefix}-caret-square-o-right:before { content: $fa-var-caret-square-o-right; } -.#{$fa-css-prefix}-euro:before, -.#{$fa-css-prefix}-eur:before { content: $fa-var-eur; } -.#{$fa-css-prefix}-gbp:before { content: $fa-var-gbp; } -.#{$fa-css-prefix}-dollar:before, -.#{$fa-css-prefix}-usd:before { content: $fa-var-usd; } -.#{$fa-css-prefix}-rupee:before, -.#{$fa-css-prefix}-inr:before { content: $fa-var-inr; } -.#{$fa-css-prefix}-cny:before, -.#{$fa-css-prefix}-rmb:before, -.#{$fa-css-prefix}-yen:before, -.#{$fa-css-prefix}-jpy:before { content: $fa-var-jpy; } -.#{$fa-css-prefix}-ruble:before, -.#{$fa-css-prefix}-rouble:before, -.#{$fa-css-prefix}-rub:before { content: $fa-var-rub; } -.#{$fa-css-prefix}-won:before, -.#{$fa-css-prefix}-krw:before { content: $fa-var-krw; } -.#{$fa-css-prefix}-bitcoin:before, -.#{$fa-css-prefix}-btc:before { content: $fa-var-btc; } -.#{$fa-css-prefix}-file:before { content: $fa-var-file; } -.#{$fa-css-prefix}-file-text:before { content: $fa-var-file-text; } -.#{$fa-css-prefix}-sort-alpha-asc:before { content: $fa-var-sort-alpha-asc; } -.#{$fa-css-prefix}-sort-alpha-desc:before { content: $fa-var-sort-alpha-desc; } -.#{$fa-css-prefix}-sort-amount-asc:before { content: $fa-var-sort-amount-asc; } -.#{$fa-css-prefix}-sort-amount-desc:before { content: $fa-var-sort-amount-desc; } -.#{$fa-css-prefix}-sort-numeric-asc:before { content: $fa-var-sort-numeric-asc; } -.#{$fa-css-prefix}-sort-numeric-desc:before { content: $fa-var-sort-numeric-desc; } -.#{$fa-css-prefix}-thumbs-up:before { content: $fa-var-thumbs-up; } -.#{$fa-css-prefix}-thumbs-down:before { content: $fa-var-thumbs-down; } -.#{$fa-css-prefix}-youtube-square:before { content: $fa-var-youtube-square; } -.#{$fa-css-prefix}-youtube:before { content: $fa-var-youtube; } -.#{$fa-css-prefix}-xing:before { content: $fa-var-xing; } -.#{$fa-css-prefix}-xing-square:before { content: $fa-var-xing-square; } -.#{$fa-css-prefix}-youtube-play:before { content: $fa-var-youtube-play; } -.#{$fa-css-prefix}-dropbox:before { content: $fa-var-dropbox; } -.#{$fa-css-prefix}-stack-overflow:before { content: $fa-var-stack-overflow; } -.#{$fa-css-prefix}-instagram:before { content: $fa-var-instagram; } -.#{$fa-css-prefix}-flickr:before { content: $fa-var-flickr; } -.#{$fa-css-prefix}-adn:before { content: $fa-var-adn; } -.#{$fa-css-prefix}-bitbucket:before { content: $fa-var-bitbucket; } -.#{$fa-css-prefix}-bitbucket-square:before { content: $fa-var-bitbucket-square; } -.#{$fa-css-prefix}-tumblr:before { content: $fa-var-tumblr; } -.#{$fa-css-prefix}-tumblr-square:before { content: $fa-var-tumblr-square; } -.#{$fa-css-prefix}-long-arrow-down:before { content: $fa-var-long-arrow-down; } -.#{$fa-css-prefix}-long-arrow-up:before { content: $fa-var-long-arrow-up; } -.#{$fa-css-prefix}-long-arrow-left:before { content: $fa-var-long-arrow-left; } -.#{$fa-css-prefix}-long-arrow-right:before { content: $fa-var-long-arrow-right; } -.#{$fa-css-prefix}-apple:before { content: $fa-var-apple; } -.#{$fa-css-prefix}-windows:before { content: $fa-var-windows; } -.#{$fa-css-prefix}-android:before { content: $fa-var-android; } -.#{$fa-css-prefix}-linux:before { content: $fa-var-linux; } -.#{$fa-css-prefix}-dribbble:before { content: $fa-var-dribbble; } -.#{$fa-css-prefix}-skype:before { content: $fa-var-skype; } -.#{$fa-css-prefix}-foursquare:before { content: $fa-var-foursquare; } -.#{$fa-css-prefix}-trello:before { content: $fa-var-trello; } -.#{$fa-css-prefix}-female:before { content: $fa-var-female; } -.#{$fa-css-prefix}-male:before { content: $fa-var-male; } -.#{$fa-css-prefix}-gittip:before, -.#{$fa-css-prefix}-gratipay:before { content: $fa-var-gratipay; } -.#{$fa-css-prefix}-sun-o:before { content: $fa-var-sun-o; } -.#{$fa-css-prefix}-moon-o:before { content: $fa-var-moon-o; } -.#{$fa-css-prefix}-archive:before { content: $fa-var-archive; } -.#{$fa-css-prefix}-bug:before { content: $fa-var-bug; } -.#{$fa-css-prefix}-vk:before { content: $fa-var-vk; } -.#{$fa-css-prefix}-weibo:before { content: $fa-var-weibo; } -.#{$fa-css-prefix}-renren:before { content: $fa-var-renren; } -.#{$fa-css-prefix}-pagelines:before { content: $fa-var-pagelines; } -.#{$fa-css-prefix}-stack-exchange:before { content: $fa-var-stack-exchange; } -.#{$fa-css-prefix}-arrow-circle-o-right:before { content: $fa-var-arrow-circle-o-right; } -.#{$fa-css-prefix}-arrow-circle-o-left:before { content: $fa-var-arrow-circle-o-left; } -.#{$fa-css-prefix}-toggle-left:before, -.#{$fa-css-prefix}-caret-square-o-left:before { content: $fa-var-caret-square-o-left; } -.#{$fa-css-prefix}-dot-circle-o:before { content: $fa-var-dot-circle-o; } -.#{$fa-css-prefix}-wheelchair:before { content: $fa-var-wheelchair; } -.#{$fa-css-prefix}-vimeo-square:before { content: $fa-var-vimeo-square; } -.#{$fa-css-prefix}-turkish-lira:before, -.#{$fa-css-prefix}-try:before { content: $fa-var-try; } -.#{$fa-css-prefix}-plus-square-o:before { content: $fa-var-plus-square-o; } -.#{$fa-css-prefix}-space-shuttle:before { content: $fa-var-space-shuttle; } -.#{$fa-css-prefix}-slack:before { content: $fa-var-slack; } -.#{$fa-css-prefix}-envelope-square:before { content: $fa-var-envelope-square; } -.#{$fa-css-prefix}-wordpress:before { content: $fa-var-wordpress; } -.#{$fa-css-prefix}-openid:before { content: $fa-var-openid; } -.#{$fa-css-prefix}-institution:before, -.#{$fa-css-prefix}-bank:before, -.#{$fa-css-prefix}-university:before { content: $fa-var-university; } -.#{$fa-css-prefix}-mortar-board:before, -.#{$fa-css-prefix}-graduation-cap:before { content: $fa-var-graduation-cap; } -.#{$fa-css-prefix}-yahoo:before { content: $fa-var-yahoo; } -.#{$fa-css-prefix}-google:before { content: $fa-var-google; } -.#{$fa-css-prefix}-reddit:before { content: $fa-var-reddit; } -.#{$fa-css-prefix}-reddit-square:before { content: $fa-var-reddit-square; } -.#{$fa-css-prefix}-stumbleupon-circle:before { content: $fa-var-stumbleupon-circle; } -.#{$fa-css-prefix}-stumbleupon:before { content: $fa-var-stumbleupon; } -.#{$fa-css-prefix}-delicious:before { content: $fa-var-delicious; } -.#{$fa-css-prefix}-digg:before { content: $fa-var-digg; } -.#{$fa-css-prefix}-pied-piper:before { content: $fa-var-pied-piper; } -.#{$fa-css-prefix}-pied-piper-alt:before { content: $fa-var-pied-piper-alt; } -.#{$fa-css-prefix}-drupal:before { content: $fa-var-drupal; } -.#{$fa-css-prefix}-joomla:before { content: $fa-var-joomla; } -.#{$fa-css-prefix}-language:before { content: $fa-var-language; } -.#{$fa-css-prefix}-fax:before { content: $fa-var-fax; } -.#{$fa-css-prefix}-building:before { content: $fa-var-building; } -.#{$fa-css-prefix}-child:before { content: $fa-var-child; } -.#{$fa-css-prefix}-paw:before { content: $fa-var-paw; } -.#{$fa-css-prefix}-spoon:before { content: $fa-var-spoon; } -.#{$fa-css-prefix}-cube:before { content: $fa-var-cube; } -.#{$fa-css-prefix}-cubes:before { content: $fa-var-cubes; } -.#{$fa-css-prefix}-behance:before { content: $fa-var-behance; } -.#{$fa-css-prefix}-behance-square:before { content: $fa-var-behance-square; } -.#{$fa-css-prefix}-steam:before { content: $fa-var-steam; } -.#{$fa-css-prefix}-steam-square:before { content: $fa-var-steam-square; } -.#{$fa-css-prefix}-recycle:before { content: $fa-var-recycle; } -.#{$fa-css-prefix}-automobile:before, -.#{$fa-css-prefix}-car:before { content: $fa-var-car; } -.#{$fa-css-prefix}-cab:before, -.#{$fa-css-prefix}-taxi:before { content: $fa-var-taxi; } -.#{$fa-css-prefix}-tree:before { content: $fa-var-tree; } -.#{$fa-css-prefix}-spotify:before { content: $fa-var-spotify; } -.#{$fa-css-prefix}-deviantart:before { content: $fa-var-deviantart; } -.#{$fa-css-prefix}-soundcloud:before { content: $fa-var-soundcloud; } -.#{$fa-css-prefix}-database:before { content: $fa-var-database; } -.#{$fa-css-prefix}-file-pdf-o:before { content: $fa-var-file-pdf-o; } -.#{$fa-css-prefix}-file-word-o:before { content: $fa-var-file-word-o; } -.#{$fa-css-prefix}-file-excel-o:before { content: $fa-var-file-excel-o; } -.#{$fa-css-prefix}-file-powerpoint-o:before { content: $fa-var-file-powerpoint-o; } -.#{$fa-css-prefix}-file-photo-o:before, -.#{$fa-css-prefix}-file-picture-o:before, -.#{$fa-css-prefix}-file-image-o:before { content: $fa-var-file-image-o; } -.#{$fa-css-prefix}-file-zip-o:before, -.#{$fa-css-prefix}-file-archive-o:before { content: $fa-var-file-archive-o; } -.#{$fa-css-prefix}-file-sound-o:before, -.#{$fa-css-prefix}-file-audio-o:before { content: $fa-var-file-audio-o; } -.#{$fa-css-prefix}-file-movie-o:before, -.#{$fa-css-prefix}-file-video-o:before { content: $fa-var-file-video-o; } -.#{$fa-css-prefix}-file-code-o:before { content: $fa-var-file-code-o; } -.#{$fa-css-prefix}-vine:before { content: $fa-var-vine; } -.#{$fa-css-prefix}-codepen:before { content: $fa-var-codepen; } -.#{$fa-css-prefix}-jsfiddle:before { content: $fa-var-jsfiddle; } -.#{$fa-css-prefix}-life-bouy:before, -.#{$fa-css-prefix}-life-buoy:before, -.#{$fa-css-prefix}-life-saver:before, -.#{$fa-css-prefix}-support:before, -.#{$fa-css-prefix}-life-ring:before { content: $fa-var-life-ring; } -.#{$fa-css-prefix}-circle-o-notch:before { content: $fa-var-circle-o-notch; } -.#{$fa-css-prefix}-ra:before, -.#{$fa-css-prefix}-rebel:before { content: $fa-var-rebel; } -.#{$fa-css-prefix}-ge:before, -.#{$fa-css-prefix}-empire:before { content: $fa-var-empire; } -.#{$fa-css-prefix}-git-square:before { content: $fa-var-git-square; } -.#{$fa-css-prefix}-git:before { content: $fa-var-git; } -.#{$fa-css-prefix}-y-combinator-square:before, -.#{$fa-css-prefix}-yc-square:before, -.#{$fa-css-prefix}-hacker-news:before { content: $fa-var-hacker-news; } -.#{$fa-css-prefix}-tencent-weibo:before { content: $fa-var-tencent-weibo; } -.#{$fa-css-prefix}-qq:before { content: $fa-var-qq; } -.#{$fa-css-prefix}-wechat:before, -.#{$fa-css-prefix}-weixin:before { content: $fa-var-weixin; } -.#{$fa-css-prefix}-send:before, -.#{$fa-css-prefix}-paper-plane:before { content: $fa-var-paper-plane; } -.#{$fa-css-prefix}-send-o:before, -.#{$fa-css-prefix}-paper-plane-o:before { content: $fa-var-paper-plane-o; } -.#{$fa-css-prefix}-history:before { content: $fa-var-history; } -.#{$fa-css-prefix}-circle-thin:before { content: $fa-var-circle-thin; } -.#{$fa-css-prefix}-header:before { content: $fa-var-header; } -.#{$fa-css-prefix}-paragraph:before { content: $fa-var-paragraph; } -.#{$fa-css-prefix}-sliders:before { content: $fa-var-sliders; } -.#{$fa-css-prefix}-share-alt:before { content: $fa-var-share-alt; } -.#{$fa-css-prefix}-share-alt-square:before { content: $fa-var-share-alt-square; } -.#{$fa-css-prefix}-bomb:before { content: $fa-var-bomb; } -.#{$fa-css-prefix}-soccer-ball-o:before, -.#{$fa-css-prefix}-futbol-o:before { content: $fa-var-futbol-o; } -.#{$fa-css-prefix}-tty:before { content: $fa-var-tty; } -.#{$fa-css-prefix}-binoculars:before { content: $fa-var-binoculars; } -.#{$fa-css-prefix}-plug:before { content: $fa-var-plug; } -.#{$fa-css-prefix}-slideshare:before { content: $fa-var-slideshare; } -.#{$fa-css-prefix}-twitch:before { content: $fa-var-twitch; } -.#{$fa-css-prefix}-yelp:before { content: $fa-var-yelp; } -.#{$fa-css-prefix}-newspaper-o:before { content: $fa-var-newspaper-o; } -.#{$fa-css-prefix}-wifi:before { content: $fa-var-wifi; } -.#{$fa-css-prefix}-calculator:before { content: $fa-var-calculator; } -.#{$fa-css-prefix}-paypal:before { content: $fa-var-paypal; } -.#{$fa-css-prefix}-google-wallet:before { content: $fa-var-google-wallet; } -.#{$fa-css-prefix}-cc-visa:before { content: $fa-var-cc-visa; } -.#{$fa-css-prefix}-cc-mastercard:before { content: $fa-var-cc-mastercard; } -.#{$fa-css-prefix}-cc-discover:before { content: $fa-var-cc-discover; } -.#{$fa-css-prefix}-cc-amex:before { content: $fa-var-cc-amex; } -.#{$fa-css-prefix}-cc-paypal:before { content: $fa-var-cc-paypal; } -.#{$fa-css-prefix}-cc-stripe:before { content: $fa-var-cc-stripe; } -.#{$fa-css-prefix}-bell-slash:before { content: $fa-var-bell-slash; } -.#{$fa-css-prefix}-bell-slash-o:before { content: $fa-var-bell-slash-o; } -.#{$fa-css-prefix}-trash:before { content: $fa-var-trash; } -.#{$fa-css-prefix}-copyright:before { content: $fa-var-copyright; } -.#{$fa-css-prefix}-at:before { content: $fa-var-at; } -.#{$fa-css-prefix}-eyedropper:before { content: $fa-var-eyedropper; } -.#{$fa-css-prefix}-paint-brush:before { content: $fa-var-paint-brush; } -.#{$fa-css-prefix}-birthday-cake:before { content: $fa-var-birthday-cake; } -.#{$fa-css-prefix}-area-chart:before { content: $fa-var-area-chart; } -.#{$fa-css-prefix}-pie-chart:before { content: $fa-var-pie-chart; } -.#{$fa-css-prefix}-line-chart:before { content: $fa-var-line-chart; } -.#{$fa-css-prefix}-lastfm:before { content: $fa-var-lastfm; } -.#{$fa-css-prefix}-lastfm-square:before { content: $fa-var-lastfm-square; } -.#{$fa-css-prefix}-toggle-off:before { content: $fa-var-toggle-off; } -.#{$fa-css-prefix}-toggle-on:before { content: $fa-var-toggle-on; } -.#{$fa-css-prefix}-bicycle:before { content: $fa-var-bicycle; } -.#{$fa-css-prefix}-bus:before { content: $fa-var-bus; } -.#{$fa-css-prefix}-ioxhost:before { content: $fa-var-ioxhost; } -.#{$fa-css-prefix}-angellist:before { content: $fa-var-angellist; } -.#{$fa-css-prefix}-cc:before { content: $fa-var-cc; } -.#{$fa-css-prefix}-shekel:before, -.#{$fa-css-prefix}-sheqel:before, -.#{$fa-css-prefix}-ils:before { content: $fa-var-ils; } -.#{$fa-css-prefix}-meanpath:before { content: $fa-var-meanpath; } -.#{$fa-css-prefix}-buysellads:before { content: $fa-var-buysellads; } -.#{$fa-css-prefix}-connectdevelop:before { content: $fa-var-connectdevelop; } -.#{$fa-css-prefix}-dashcube:before { content: $fa-var-dashcube; } -.#{$fa-css-prefix}-forumbee:before { content: $fa-var-forumbee; } -.#{$fa-css-prefix}-leanpub:before { content: $fa-var-leanpub; } -.#{$fa-css-prefix}-sellsy:before { content: $fa-var-sellsy; } -.#{$fa-css-prefix}-shirtsinbulk:before { content: $fa-var-shirtsinbulk; } -.#{$fa-css-prefix}-simplybuilt:before { content: $fa-var-simplybuilt; } -.#{$fa-css-prefix}-skyatlas:before { content: $fa-var-skyatlas; } -.#{$fa-css-prefix}-cart-plus:before { content: $fa-var-cart-plus; } -.#{$fa-css-prefix}-cart-arrow-down:before { content: $fa-var-cart-arrow-down; } -.#{$fa-css-prefix}-diamond:before { content: $fa-var-diamond; } -.#{$fa-css-prefix}-ship:before { content: $fa-var-ship; } -.#{$fa-css-prefix}-user-secret:before { content: $fa-var-user-secret; } -.#{$fa-css-prefix}-motorcycle:before { content: $fa-var-motorcycle; } -.#{$fa-css-prefix}-street-view:before { content: $fa-var-street-view; } -.#{$fa-css-prefix}-heartbeat:before { content: $fa-var-heartbeat; } -.#{$fa-css-prefix}-venus:before { content: $fa-var-venus; } -.#{$fa-css-prefix}-mars:before { content: $fa-var-mars; } -.#{$fa-css-prefix}-mercury:before { content: $fa-var-mercury; } -.#{$fa-css-prefix}-intersex:before, -.#{$fa-css-prefix}-transgender:before { content: $fa-var-transgender; } -.#{$fa-css-prefix}-transgender-alt:before { content: $fa-var-transgender-alt; } -.#{$fa-css-prefix}-venus-double:before { content: $fa-var-venus-double; } -.#{$fa-css-prefix}-mars-double:before { content: $fa-var-mars-double; } -.#{$fa-css-prefix}-venus-mars:before { content: $fa-var-venus-mars; } -.#{$fa-css-prefix}-mars-stroke:before { content: $fa-var-mars-stroke; } -.#{$fa-css-prefix}-mars-stroke-v:before { content: $fa-var-mars-stroke-v; } -.#{$fa-css-prefix}-mars-stroke-h:before { content: $fa-var-mars-stroke-h; } -.#{$fa-css-prefix}-neuter:before { content: $fa-var-neuter; } -.#{$fa-css-prefix}-genderless:before { content: $fa-var-genderless; } -.#{$fa-css-prefix}-facebook-official:before { content: $fa-var-facebook-official; } -.#{$fa-css-prefix}-pinterest-p:before { content: $fa-var-pinterest-p; } -.#{$fa-css-prefix}-whatsapp:before { content: $fa-var-whatsapp; } -.#{$fa-css-prefix}-server:before { content: $fa-var-server; } -.#{$fa-css-prefix}-user-plus:before { content: $fa-var-user-plus; } -.#{$fa-css-prefix}-user-times:before { content: $fa-var-user-times; } -.#{$fa-css-prefix}-hotel:before, -.#{$fa-css-prefix}-bed:before { content: $fa-var-bed; } -.#{$fa-css-prefix}-viacoin:before { content: $fa-var-viacoin; } -.#{$fa-css-prefix}-train:before { content: $fa-var-train; } -.#{$fa-css-prefix}-subway:before { content: $fa-var-subway; } -.#{$fa-css-prefix}-medium:before { content: $fa-var-medium; } -.#{$fa-css-prefix}-yc:before, -.#{$fa-css-prefix}-y-combinator:before { content: $fa-var-y-combinator; } -.#{$fa-css-prefix}-optin-monster:before { content: $fa-var-optin-monster; } -.#{$fa-css-prefix}-opencart:before { content: $fa-var-opencart; } -.#{$fa-css-prefix}-expeditedssl:before { content: $fa-var-expeditedssl; } -.#{$fa-css-prefix}-battery-4:before, -.#{$fa-css-prefix}-battery-full:before { content: $fa-var-battery-full; } -.#{$fa-css-prefix}-battery-3:before, -.#{$fa-css-prefix}-battery-three-quarters:before { content: $fa-var-battery-three-quarters; } -.#{$fa-css-prefix}-battery-2:before, -.#{$fa-css-prefix}-battery-half:before { content: $fa-var-battery-half; } -.#{$fa-css-prefix}-battery-1:before, -.#{$fa-css-prefix}-battery-quarter:before { content: $fa-var-battery-quarter; } -.#{$fa-css-prefix}-battery-0:before, -.#{$fa-css-prefix}-battery-empty:before { content: $fa-var-battery-empty; } -.#{$fa-css-prefix}-mouse-pointer:before { content: $fa-var-mouse-pointer; } -.#{$fa-css-prefix}-i-cursor:before { content: $fa-var-i-cursor; } -.#{$fa-css-prefix}-object-group:before { content: $fa-var-object-group; } -.#{$fa-css-prefix}-object-ungroup:before { content: $fa-var-object-ungroup; } -.#{$fa-css-prefix}-sticky-note:before { content: $fa-var-sticky-note; } -.#{$fa-css-prefix}-sticky-note-o:before { content: $fa-var-sticky-note-o; } -.#{$fa-css-prefix}-cc-jcb:before { content: $fa-var-cc-jcb; } -.#{$fa-css-prefix}-cc-diners-club:before { content: $fa-var-cc-diners-club; } -.#{$fa-css-prefix}-clone:before { content: $fa-var-clone; } -.#{$fa-css-prefix}-balance-scale:before { content: $fa-var-balance-scale; } -.#{$fa-css-prefix}-hourglass-o:before { content: $fa-var-hourglass-o; } -.#{$fa-css-prefix}-hourglass-1:before, -.#{$fa-css-prefix}-hourglass-start:before { content: $fa-var-hourglass-start; } -.#{$fa-css-prefix}-hourglass-2:before, -.#{$fa-css-prefix}-hourglass-half:before { content: $fa-var-hourglass-half; } -.#{$fa-css-prefix}-hourglass-3:before, -.#{$fa-css-prefix}-hourglass-end:before { content: $fa-var-hourglass-end; } -.#{$fa-css-prefix}-hourglass:before { content: $fa-var-hourglass; } -.#{$fa-css-prefix}-hand-grab-o:before, -.#{$fa-css-prefix}-hand-rock-o:before { content: $fa-var-hand-rock-o; } -.#{$fa-css-prefix}-hand-stop-o:before, -.#{$fa-css-prefix}-hand-paper-o:before { content: $fa-var-hand-paper-o; } -.#{$fa-css-prefix}-hand-scissors-o:before { content: $fa-var-hand-scissors-o; } -.#{$fa-css-prefix}-hand-lizard-o:before { content: $fa-var-hand-lizard-o; } -.#{$fa-css-prefix}-hand-spock-o:before { content: $fa-var-hand-spock-o; } -.#{$fa-css-prefix}-hand-pointer-o:before { content: $fa-var-hand-pointer-o; } -.#{$fa-css-prefix}-hand-peace-o:before { content: $fa-var-hand-peace-o; } -.#{$fa-css-prefix}-trademark:before { content: $fa-var-trademark; } -.#{$fa-css-prefix}-registered:before { content: $fa-var-registered; } -.#{$fa-css-prefix}-creative-commons:before { content: $fa-var-creative-commons; } -.#{$fa-css-prefix}-gg:before { content: $fa-var-gg; } -.#{$fa-css-prefix}-gg-circle:before { content: $fa-var-gg-circle; } -.#{$fa-css-prefix}-tripadvisor:before { content: $fa-var-tripadvisor; } -.#{$fa-css-prefix}-odnoklassniki:before { content: $fa-var-odnoklassniki; } -.#{$fa-css-prefix}-odnoklassniki-square:before { content: $fa-var-odnoklassniki-square; } -.#{$fa-css-prefix}-get-pocket:before { content: $fa-var-get-pocket; } -.#{$fa-css-prefix}-wikipedia-w:before { content: $fa-var-wikipedia-w; } -.#{$fa-css-prefix}-safari:before { content: $fa-var-safari; } -.#{$fa-css-prefix}-chrome:before { content: $fa-var-chrome; } -.#{$fa-css-prefix}-firefox:before { content: $fa-var-firefox; } -.#{$fa-css-prefix}-opera:before { content: $fa-var-opera; } -.#{$fa-css-prefix}-internet-explorer:before { content: $fa-var-internet-explorer; } -.#{$fa-css-prefix}-tv:before, -.#{$fa-css-prefix}-television:before { content: $fa-var-television; } -.#{$fa-css-prefix}-contao:before { content: $fa-var-contao; } -.#{$fa-css-prefix}-500px:before { content: $fa-var-500px; } -.#{$fa-css-prefix}-amazon:before { content: $fa-var-amazon; } -.#{$fa-css-prefix}-calendar-plus-o:before { content: $fa-var-calendar-plus-o; } -.#{$fa-css-prefix}-calendar-minus-o:before { content: $fa-var-calendar-minus-o; } -.#{$fa-css-prefix}-calendar-times-o:before { content: $fa-var-calendar-times-o; } -.#{$fa-css-prefix}-calendar-check-o:before { content: $fa-var-calendar-check-o; } -.#{$fa-css-prefix}-industry:before { content: $fa-var-industry; } -.#{$fa-css-prefix}-map-pin:before { content: $fa-var-map-pin; } -.#{$fa-css-prefix}-map-signs:before { content: $fa-var-map-signs; } -.#{$fa-css-prefix}-map-o:before { content: $fa-var-map-o; } -.#{$fa-css-prefix}-map:before { content: $fa-var-map; } -.#{$fa-css-prefix}-commenting:before { content: $fa-var-commenting; } -.#{$fa-css-prefix}-commenting-o:before { content: $fa-var-commenting-o; } -.#{$fa-css-prefix}-houzz:before { content: $fa-var-houzz; } -.#{$fa-css-prefix}-vimeo:before { content: $fa-var-vimeo; } -.#{$fa-css-prefix}-black-tie:before { content: $fa-var-black-tie; } -.#{$fa-css-prefix}-fonticons:before { content: $fa-var-fonticons; } -.#{$fa-css-prefix}-reddit-alien:before { content: $fa-var-reddit-alien; } -.#{$fa-css-prefix}-edge:before { content: $fa-var-edge; } -.#{$fa-css-prefix}-credit-card-alt:before { content: $fa-var-credit-card-alt; } -.#{$fa-css-prefix}-codiepie:before { content: $fa-var-codiepie; } -.#{$fa-css-prefix}-modx:before { content: $fa-var-modx; } -.#{$fa-css-prefix}-fort-awesome:before { content: $fa-var-fort-awesome; } -.#{$fa-css-prefix}-usb:before { content: $fa-var-usb; } -.#{$fa-css-prefix}-product-hunt:before { content: $fa-var-product-hunt; } -.#{$fa-css-prefix}-mixcloud:before { content: $fa-var-mixcloud; } -.#{$fa-css-prefix}-scribd:before { content: $fa-var-scribd; } -.#{$fa-css-prefix}-pause-circle:before { content: $fa-var-pause-circle; } -.#{$fa-css-prefix}-pause-circle-o:before { content: $fa-var-pause-circle-o; } -.#{$fa-css-prefix}-stop-circle:before { content: $fa-var-stop-circle; } -.#{$fa-css-prefix}-stop-circle-o:before { content: $fa-var-stop-circle-o; } -.#{$fa-css-prefix}-shopping-bag:before { content: $fa-var-shopping-bag; } -.#{$fa-css-prefix}-shopping-basket:before { content: $fa-var-shopping-basket; } -.#{$fa-css-prefix}-hashtag:before { content: $fa-var-hashtag; } -.#{$fa-css-prefix}-bluetooth:before { content: $fa-var-bluetooth; } -.#{$fa-css-prefix}-bluetooth-b:before { content: $fa-var-bluetooth-b; } -.#{$fa-css-prefix}-percent:before { content: $fa-var-percent; } -.#{$fa-css-prefix}-gitlab:before { content: $fa-var-gitlab; } -.#{$fa-css-prefix}-wpbeginner:before { content: $fa-var-wpbeginner; } -.#{$fa-css-prefix}-wpforms:before { content: $fa-var-wpforms; } -.#{$fa-css-prefix}-envira:before { content: $fa-var-envira; } -.#{$fa-css-prefix}-universal-access:before { content: $fa-var-universal-access; } -.#{$fa-css-prefix}-wheelchair-alt:before { content: $fa-var-wheelchair-alt; } -.#{$fa-css-prefix}-question-circle-o:before { content: $fa-var-question-circle-o; } -.#{$fa-css-prefix}-blind:before { content: $fa-var-blind; } -.#{$fa-css-prefix}-audio-description:before { content: $fa-var-audio-description; } -.#{$fa-css-prefix}-volume-control-phone:before { content: $fa-var-volume-control-phone; } -.#{$fa-css-prefix}-braille:before { content: $fa-var-braille; } -.#{$fa-css-prefix}-assistive-listening-systems:before { content: $fa-var-assistive-listening-systems; } -.#{$fa-css-prefix}-asl-interpreting:before, -.#{$fa-css-prefix}-american-sign-language-interpreting:before { content: $fa-var-american-sign-language-interpreting; } -.#{$fa-css-prefix}-deafness:before, -.#{$fa-css-prefix}-hard-of-hearing:before, -.#{$fa-css-prefix}-deaf:before { content: $fa-var-deaf; } -.#{$fa-css-prefix}-glide:before { content: $fa-var-glide; } -.#{$fa-css-prefix}-glide-g:before { content: $fa-var-glide-g; } -.#{$fa-css-prefix}-signing:before, -.#{$fa-css-prefix}-sign-language:before { content: $fa-var-sign-language; } -.#{$fa-css-prefix}-low-vision:before { content: $fa-var-low-vision; } -.#{$fa-css-prefix}-viadeo:before { content: $fa-var-viadeo; } -.#{$fa-css-prefix}-viadeo-square:before { content: $fa-var-viadeo-square; } -.#{$fa-css-prefix}-snapchat:before { content: $fa-var-snapchat; } -.#{$fa-css-prefix}-snapchat-ghost:before { content: $fa-var-snapchat-ghost; } -.#{$fa-css-prefix}-snapchat-square:before { content: $fa-var-snapchat-square; } diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_larger.scss b/iam-login-service/src/main/webapp/resources/font-awesome/scss/_larger.scss deleted file mode 100644 index 41e9a8184..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_larger.scss +++ /dev/null @@ -1,13 +0,0 @@ -// Icon Sizes -// ------------------------- - -/* makes the font 33% larger relative to the icon container */ -.#{$fa-css-prefix}-lg { - font-size: (4em / 3); - line-height: (3em / 4); - vertical-align: -15%; -} -.#{$fa-css-prefix}-2x { font-size: 2em; } -.#{$fa-css-prefix}-3x { font-size: 3em; } -.#{$fa-css-prefix}-4x { font-size: 4em; } -.#{$fa-css-prefix}-5x { font-size: 5em; } diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_list.scss b/iam-login-service/src/main/webapp/resources/font-awesome/scss/_list.scss deleted file mode 100644 index 7d1e4d54d..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_list.scss +++ /dev/null @@ -1,19 +0,0 @@ -// List Icons -// ------------------------- - -.#{$fa-css-prefix}-ul { - padding-left: 0; - margin-left: $fa-li-width; - list-style-type: none; - > li { position: relative; } -} -.#{$fa-css-prefix}-li { - position: absolute; - left: -$fa-li-width; - width: $fa-li-width; - top: (2em / 14); - text-align: center; - &.#{$fa-css-prefix}-lg { - left: -$fa-li-width + (4em / 14); - } -} diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_mixins.scss b/iam-login-service/src/main/webapp/resources/font-awesome/scss/_mixins.scss deleted file mode 100644 index c3bbd5745..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_mixins.scss +++ /dev/null @@ -1,60 +0,0 @@ -// Mixins -// -------------------------- - -@mixin fa-icon() { - display: inline-block; - font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration - font-size: inherit; // can't have font-size inherit on line above, so need to override - text-rendering: auto; // optimizelegibility throws things off #1094 - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -} - -@mixin fa-icon-rotate($degrees, $rotation) { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; - -webkit-transform: rotate($degrees); - -ms-transform: rotate($degrees); - transform: rotate($degrees); -} - -@mixin fa-icon-flip($horiz, $vert, $rotation) { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; - -webkit-transform: scale($horiz, $vert); - -ms-transform: scale($horiz, $vert); - transform: scale($horiz, $vert); -} - - -// Only display content to screen readers. A la Bootstrap 4. -// -// See: http://a11yproject.com/posts/how-to-hide-content/ - -@mixin sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0,0,0,0); - border: 0; -} - -// Use in conjunction with .sr-only to only display content when it's focused. -// -// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 -// -// Credit: HTML5 Boilerplate - -@mixin sr-only-focusable { - &:active, - &:focus { - position: static; - width: auto; - height: auto; - margin: 0; - overflow: visible; - clip: auto; - } -} diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_path.scss b/iam-login-service/src/main/webapp/resources/font-awesome/scss/_path.scss deleted file mode 100644 index bb457c23a..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_path.scss +++ /dev/null @@ -1,15 +0,0 @@ -/* FONT PATH - * -------------------------- */ - -@font-face { - font-family: 'FontAwesome'; - src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); - src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), - url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), - url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), - url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), - url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); -// src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts - font-weight: normal; - font-style: normal; -} diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_rotated-flipped.scss b/iam-login-service/src/main/webapp/resources/font-awesome/scss/_rotated-flipped.scss deleted file mode 100644 index a3558fd09..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_rotated-flipped.scss +++ /dev/null @@ -1,20 +0,0 @@ -// Rotated & Flipped Icons -// ------------------------- - -.#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } -.#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } -.#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } - -.#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } -.#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } - -// Hook for IE8-9 -// ------------------------- - -:root .#{$fa-css-prefix}-rotate-90, -:root .#{$fa-css-prefix}-rotate-180, -:root .#{$fa-css-prefix}-rotate-270, -:root .#{$fa-css-prefix}-flip-horizontal, -:root .#{$fa-css-prefix}-flip-vertical { - filter: none; -} diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_screen-reader.scss b/iam-login-service/src/main/webapp/resources/font-awesome/scss/_screen-reader.scss deleted file mode 100644 index 637426f0d..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_screen-reader.scss +++ /dev/null @@ -1,5 +0,0 @@ -// Screen Readers -// ------------------------- - -.sr-only { @include sr-only(); } -.sr-only-focusable { @include sr-only-focusable(); } diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_stacked.scss b/iam-login-service/src/main/webapp/resources/font-awesome/scss/_stacked.scss deleted file mode 100644 index aef740366..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_stacked.scss +++ /dev/null @@ -1,20 +0,0 @@ -// Stacked Icons -// ------------------------- - -.#{$fa-css-prefix}-stack { - position: relative; - display: inline-block; - width: 2em; - height: 2em; - line-height: 2em; - vertical-align: middle; -} -.#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { - position: absolute; - left: 0; - width: 100%; - text-align: center; -} -.#{$fa-css-prefix}-stack-1x { line-height: inherit; } -.#{$fa-css-prefix}-stack-2x { font-size: 2em; } -.#{$fa-css-prefix}-inverse { color: $fa-inverse; } diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_variables.scss b/iam-login-service/src/main/webapp/resources/font-awesome/scss/_variables.scss deleted file mode 100644 index 1f374d6c9..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/scss/_variables.scss +++ /dev/null @@ -1,735 +0,0 @@ -// Variables -// -------------------------- - -$fa-font-path: "../fonts" !default; -$fa-font-size-base: 14px !default; -$fa-line-height-base: 1 !default; -//$fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.6.1/fonts" !default; // for referencing Bootstrap CDN font files directly -$fa-css-prefix: fa !default; -$fa-version: "4.6.1" !default; -$fa-border-color: #eee !default; -$fa-inverse: #fff !default; -$fa-li-width: (30em / 14) !default; - -$fa-var-500px: "\f26e"; -$fa-var-adjust: "\f042"; -$fa-var-adn: "\f170"; -$fa-var-align-center: "\f037"; -$fa-var-align-justify: "\f039"; -$fa-var-align-left: "\f036"; -$fa-var-align-right: "\f038"; -$fa-var-amazon: "\f270"; -$fa-var-ambulance: "\f0f9"; -$fa-var-american-sign-language-interpreting: "\f2a3"; -$fa-var-anchor: "\f13d"; -$fa-var-android: "\f17b"; -$fa-var-angellist: "\f209"; -$fa-var-angle-double-down: "\f103"; -$fa-var-angle-double-left: "\f100"; -$fa-var-angle-double-right: "\f101"; -$fa-var-angle-double-up: "\f102"; -$fa-var-angle-down: "\f107"; -$fa-var-angle-left: "\f104"; -$fa-var-angle-right: "\f105"; -$fa-var-angle-up: "\f106"; -$fa-var-apple: "\f179"; -$fa-var-archive: "\f187"; -$fa-var-area-chart: "\f1fe"; -$fa-var-arrow-circle-down: "\f0ab"; -$fa-var-arrow-circle-left: "\f0a8"; -$fa-var-arrow-circle-o-down: "\f01a"; -$fa-var-arrow-circle-o-left: "\f190"; -$fa-var-arrow-circle-o-right: "\f18e"; -$fa-var-arrow-circle-o-up: "\f01b"; -$fa-var-arrow-circle-right: "\f0a9"; -$fa-var-arrow-circle-up: "\f0aa"; -$fa-var-arrow-down: "\f063"; -$fa-var-arrow-left: "\f060"; -$fa-var-arrow-right: "\f061"; -$fa-var-arrow-up: "\f062"; -$fa-var-arrows: "\f047"; -$fa-var-arrows-alt: "\f0b2"; -$fa-var-arrows-h: "\f07e"; -$fa-var-arrows-v: "\f07d"; -$fa-var-asl-interpreting: "\f2a3"; -$fa-var-assistive-listening-systems: "\f2a2"; -$fa-var-asterisk: "\f069"; -$fa-var-at: "\f1fa"; -$fa-var-audio-description: "\f29e"; -$fa-var-automobile: "\f1b9"; -$fa-var-backward: "\f04a"; -$fa-var-balance-scale: "\f24e"; -$fa-var-ban: "\f05e"; -$fa-var-bank: "\f19c"; -$fa-var-bar-chart: "\f080"; -$fa-var-bar-chart-o: "\f080"; -$fa-var-barcode: "\f02a"; -$fa-var-bars: "\f0c9"; -$fa-var-battery-0: "\f244"; -$fa-var-battery-1: "\f243"; -$fa-var-battery-2: "\f242"; -$fa-var-battery-3: "\f241"; -$fa-var-battery-4: "\f240"; -$fa-var-battery-empty: "\f244"; -$fa-var-battery-full: "\f240"; -$fa-var-battery-half: "\f242"; -$fa-var-battery-quarter: "\f243"; -$fa-var-battery-three-quarters: "\f241"; -$fa-var-bed: "\f236"; -$fa-var-beer: "\f0fc"; -$fa-var-behance: "\f1b4"; -$fa-var-behance-square: "\f1b5"; -$fa-var-bell: "\f0f3"; -$fa-var-bell-o: "\f0a2"; -$fa-var-bell-slash: "\f1f6"; -$fa-var-bell-slash-o: "\f1f7"; -$fa-var-bicycle: "\f206"; -$fa-var-binoculars: "\f1e5"; -$fa-var-birthday-cake: "\f1fd"; -$fa-var-bitbucket: "\f171"; -$fa-var-bitbucket-square: "\f172"; -$fa-var-bitcoin: "\f15a"; -$fa-var-black-tie: "\f27e"; -$fa-var-blind: "\f29d"; -$fa-var-bluetooth: "\f293"; -$fa-var-bluetooth-b: "\f294"; -$fa-var-bold: "\f032"; -$fa-var-bolt: "\f0e7"; -$fa-var-bomb: "\f1e2"; -$fa-var-book: "\f02d"; -$fa-var-bookmark: "\f02e"; -$fa-var-bookmark-o: "\f097"; -$fa-var-braille: "\f2a1"; -$fa-var-briefcase: "\f0b1"; -$fa-var-btc: "\f15a"; -$fa-var-bug: "\f188"; -$fa-var-building: "\f1ad"; -$fa-var-building-o: "\f0f7"; -$fa-var-bullhorn: "\f0a1"; -$fa-var-bullseye: "\f140"; -$fa-var-bus: "\f207"; -$fa-var-buysellads: "\f20d"; -$fa-var-cab: "\f1ba"; -$fa-var-calculator: "\f1ec"; -$fa-var-calendar: "\f073"; -$fa-var-calendar-check-o: "\f274"; -$fa-var-calendar-minus-o: "\f272"; -$fa-var-calendar-o: "\f133"; -$fa-var-calendar-plus-o: "\f271"; -$fa-var-calendar-times-o: "\f273"; -$fa-var-camera: "\f030"; -$fa-var-camera-retro: "\f083"; -$fa-var-car: "\f1b9"; -$fa-var-caret-down: "\f0d7"; -$fa-var-caret-left: "\f0d9"; -$fa-var-caret-right: "\f0da"; -$fa-var-caret-square-o-down: "\f150"; -$fa-var-caret-square-o-left: "\f191"; -$fa-var-caret-square-o-right: "\f152"; -$fa-var-caret-square-o-up: "\f151"; -$fa-var-caret-up: "\f0d8"; -$fa-var-cart-arrow-down: "\f218"; -$fa-var-cart-plus: "\f217"; -$fa-var-cc: "\f20a"; -$fa-var-cc-amex: "\f1f3"; -$fa-var-cc-diners-club: "\f24c"; -$fa-var-cc-discover: "\f1f2"; -$fa-var-cc-jcb: "\f24b"; -$fa-var-cc-mastercard: "\f1f1"; -$fa-var-cc-paypal: "\f1f4"; -$fa-var-cc-stripe: "\f1f5"; -$fa-var-cc-visa: "\f1f0"; -$fa-var-certificate: "\f0a3"; -$fa-var-chain: "\f0c1"; -$fa-var-chain-broken: "\f127"; -$fa-var-check: "\f00c"; -$fa-var-check-circle: "\f058"; -$fa-var-check-circle-o: "\f05d"; -$fa-var-check-square: "\f14a"; -$fa-var-check-square-o: "\f046"; -$fa-var-chevron-circle-down: "\f13a"; -$fa-var-chevron-circle-left: "\f137"; -$fa-var-chevron-circle-right: "\f138"; -$fa-var-chevron-circle-up: "\f139"; -$fa-var-chevron-down: "\f078"; -$fa-var-chevron-left: "\f053"; -$fa-var-chevron-right: "\f054"; -$fa-var-chevron-up: "\f077"; -$fa-var-child: "\f1ae"; -$fa-var-chrome: "\f268"; -$fa-var-circle: "\f111"; -$fa-var-circle-o: "\f10c"; -$fa-var-circle-o-notch: "\f1ce"; -$fa-var-circle-thin: "\f1db"; -$fa-var-clipboard: "\f0ea"; -$fa-var-clock-o: "\f017"; -$fa-var-clone: "\f24d"; -$fa-var-close: "\f00d"; -$fa-var-cloud: "\f0c2"; -$fa-var-cloud-download: "\f0ed"; -$fa-var-cloud-upload: "\f0ee"; -$fa-var-cny: "\f157"; -$fa-var-code: "\f121"; -$fa-var-code-fork: "\f126"; -$fa-var-codepen: "\f1cb"; -$fa-var-codiepie: "\f284"; -$fa-var-coffee: "\f0f4"; -$fa-var-cog: "\f013"; -$fa-var-cogs: "\f085"; -$fa-var-columns: "\f0db"; -$fa-var-comment: "\f075"; -$fa-var-comment-o: "\f0e5"; -$fa-var-commenting: "\f27a"; -$fa-var-commenting-o: "\f27b"; -$fa-var-comments: "\f086"; -$fa-var-comments-o: "\f0e6"; -$fa-var-compass: "\f14e"; -$fa-var-compress: "\f066"; -$fa-var-connectdevelop: "\f20e"; -$fa-var-contao: "\f26d"; -$fa-var-copy: "\f0c5"; -$fa-var-copyright: "\f1f9"; -$fa-var-creative-commons: "\f25e"; -$fa-var-credit-card: "\f09d"; -$fa-var-credit-card-alt: "\f283"; -$fa-var-crop: "\f125"; -$fa-var-crosshairs: "\f05b"; -$fa-var-css3: "\f13c"; -$fa-var-cube: "\f1b2"; -$fa-var-cubes: "\f1b3"; -$fa-var-cut: "\f0c4"; -$fa-var-cutlery: "\f0f5"; -$fa-var-dashboard: "\f0e4"; -$fa-var-dashcube: "\f210"; -$fa-var-database: "\f1c0"; -$fa-var-deaf: "\f2a4"; -$fa-var-deafness: "\f2a4"; -$fa-var-dedent: "\f03b"; -$fa-var-delicious: "\f1a5"; -$fa-var-desktop: "\f108"; -$fa-var-deviantart: "\f1bd"; -$fa-var-diamond: "\f219"; -$fa-var-digg: "\f1a6"; -$fa-var-dollar: "\f155"; -$fa-var-dot-circle-o: "\f192"; -$fa-var-download: "\f019"; -$fa-var-dribbble: "\f17d"; -$fa-var-dropbox: "\f16b"; -$fa-var-drupal: "\f1a9"; -$fa-var-edge: "\f282"; -$fa-var-edit: "\f044"; -$fa-var-eject: "\f052"; -$fa-var-ellipsis-h: "\f141"; -$fa-var-ellipsis-v: "\f142"; -$fa-var-empire: "\f1d1"; -$fa-var-envelope: "\f0e0"; -$fa-var-envelope-o: "\f003"; -$fa-var-envelope-square: "\f199"; -$fa-var-envira: "\f299"; -$fa-var-eraser: "\f12d"; -$fa-var-eur: "\f153"; -$fa-var-euro: "\f153"; -$fa-var-exchange: "\f0ec"; -$fa-var-exclamation: "\f12a"; -$fa-var-exclamation-circle: "\f06a"; -$fa-var-exclamation-triangle: "\f071"; -$fa-var-expand: "\f065"; -$fa-var-expeditedssl: "\f23e"; -$fa-var-external-link: "\f08e"; -$fa-var-external-link-square: "\f14c"; -$fa-var-eye: "\f06e"; -$fa-var-eye-slash: "\f070"; -$fa-var-eyedropper: "\f1fb"; -$fa-var-facebook: "\f09a"; -$fa-var-facebook-f: "\f09a"; -$fa-var-facebook-official: "\f230"; -$fa-var-facebook-square: "\f082"; -$fa-var-fast-backward: "\f049"; -$fa-var-fast-forward: "\f050"; -$fa-var-fax: "\f1ac"; -$fa-var-feed: "\f09e"; -$fa-var-female: "\f182"; -$fa-var-fighter-jet: "\f0fb"; -$fa-var-file: "\f15b"; -$fa-var-file-archive-o: "\f1c6"; -$fa-var-file-audio-o: "\f1c7"; -$fa-var-file-code-o: "\f1c9"; -$fa-var-file-excel-o: "\f1c3"; -$fa-var-file-image-o: "\f1c5"; -$fa-var-file-movie-o: "\f1c8"; -$fa-var-file-o: "\f016"; -$fa-var-file-pdf-o: "\f1c1"; -$fa-var-file-photo-o: "\f1c5"; -$fa-var-file-picture-o: "\f1c5"; -$fa-var-file-powerpoint-o: "\f1c4"; -$fa-var-file-sound-o: "\f1c7"; -$fa-var-file-text: "\f15c"; -$fa-var-file-text-o: "\f0f6"; -$fa-var-file-video-o: "\f1c8"; -$fa-var-file-word-o: "\f1c2"; -$fa-var-file-zip-o: "\f1c6"; -$fa-var-files-o: "\f0c5"; -$fa-var-film: "\f008"; -$fa-var-filter: "\f0b0"; -$fa-var-fire: "\f06d"; -$fa-var-fire-extinguisher: "\f134"; -$fa-var-firefox: "\f269"; -$fa-var-flag: "\f024"; -$fa-var-flag-checkered: "\f11e"; -$fa-var-flag-o: "\f11d"; -$fa-var-flash: "\f0e7"; -$fa-var-flask: "\f0c3"; -$fa-var-flickr: "\f16e"; -$fa-var-floppy-o: "\f0c7"; -$fa-var-folder: "\f07b"; -$fa-var-folder-o: "\f114"; -$fa-var-folder-open: "\f07c"; -$fa-var-folder-open-o: "\f115"; -$fa-var-font: "\f031"; -$fa-var-fonticons: "\f280"; -$fa-var-fort-awesome: "\f286"; -$fa-var-forumbee: "\f211"; -$fa-var-forward: "\f04e"; -$fa-var-foursquare: "\f180"; -$fa-var-frown-o: "\f119"; -$fa-var-futbol-o: "\f1e3"; -$fa-var-gamepad: "\f11b"; -$fa-var-gavel: "\f0e3"; -$fa-var-gbp: "\f154"; -$fa-var-ge: "\f1d1"; -$fa-var-gear: "\f013"; -$fa-var-gears: "\f085"; -$fa-var-genderless: "\f22d"; -$fa-var-get-pocket: "\f265"; -$fa-var-gg: "\f260"; -$fa-var-gg-circle: "\f261"; -$fa-var-gift: "\f06b"; -$fa-var-git: "\f1d3"; -$fa-var-git-square: "\f1d2"; -$fa-var-github: "\f09b"; -$fa-var-github-alt: "\f113"; -$fa-var-github-square: "\f092"; -$fa-var-gitlab: "\f296"; -$fa-var-gittip: "\f184"; -$fa-var-glass: "\f000"; -$fa-var-glide: "\f2a5"; -$fa-var-glide-g: "\f2a6"; -$fa-var-globe: "\f0ac"; -$fa-var-google: "\f1a0"; -$fa-var-google-plus: "\f0d5"; -$fa-var-google-plus-square: "\f0d4"; -$fa-var-google-wallet: "\f1ee"; -$fa-var-graduation-cap: "\f19d"; -$fa-var-gratipay: "\f184"; -$fa-var-group: "\f0c0"; -$fa-var-h-square: "\f0fd"; -$fa-var-hacker-news: "\f1d4"; -$fa-var-hand-grab-o: "\f255"; -$fa-var-hand-lizard-o: "\f258"; -$fa-var-hand-o-down: "\f0a7"; -$fa-var-hand-o-left: "\f0a5"; -$fa-var-hand-o-right: "\f0a4"; -$fa-var-hand-o-up: "\f0a6"; -$fa-var-hand-paper-o: "\f256"; -$fa-var-hand-peace-o: "\f25b"; -$fa-var-hand-pointer-o: "\f25a"; -$fa-var-hand-rock-o: "\f255"; -$fa-var-hand-scissors-o: "\f257"; -$fa-var-hand-spock-o: "\f259"; -$fa-var-hand-stop-o: "\f256"; -$fa-var-hard-of-hearing: "\f2a4"; -$fa-var-hashtag: "\f292"; -$fa-var-hdd-o: "\f0a0"; -$fa-var-header: "\f1dc"; -$fa-var-headphones: "\f025"; -$fa-var-heart: "\f004"; -$fa-var-heart-o: "\f08a"; -$fa-var-heartbeat: "\f21e"; -$fa-var-history: "\f1da"; -$fa-var-home: "\f015"; -$fa-var-hospital-o: "\f0f8"; -$fa-var-hotel: "\f236"; -$fa-var-hourglass: "\f254"; -$fa-var-hourglass-1: "\f251"; -$fa-var-hourglass-2: "\f252"; -$fa-var-hourglass-3: "\f253"; -$fa-var-hourglass-end: "\f253"; -$fa-var-hourglass-half: "\f252"; -$fa-var-hourglass-o: "\f250"; -$fa-var-hourglass-start: "\f251"; -$fa-var-houzz: "\f27c"; -$fa-var-html5: "\f13b"; -$fa-var-i-cursor: "\f246"; -$fa-var-ils: "\f20b"; -$fa-var-image: "\f03e"; -$fa-var-inbox: "\f01c"; -$fa-var-indent: "\f03c"; -$fa-var-industry: "\f275"; -$fa-var-info: "\f129"; -$fa-var-info-circle: "\f05a"; -$fa-var-inr: "\f156"; -$fa-var-instagram: "\f16d"; -$fa-var-institution: "\f19c"; -$fa-var-internet-explorer: "\f26b"; -$fa-var-intersex: "\f224"; -$fa-var-ioxhost: "\f208"; -$fa-var-italic: "\f033"; -$fa-var-joomla: "\f1aa"; -$fa-var-jpy: "\f157"; -$fa-var-jsfiddle: "\f1cc"; -$fa-var-key: "\f084"; -$fa-var-keyboard-o: "\f11c"; -$fa-var-krw: "\f159"; -$fa-var-language: "\f1ab"; -$fa-var-laptop: "\f109"; -$fa-var-lastfm: "\f202"; -$fa-var-lastfm-square: "\f203"; -$fa-var-leaf: "\f06c"; -$fa-var-leanpub: "\f212"; -$fa-var-legal: "\f0e3"; -$fa-var-lemon-o: "\f094"; -$fa-var-level-down: "\f149"; -$fa-var-level-up: "\f148"; -$fa-var-life-bouy: "\f1cd"; -$fa-var-life-buoy: "\f1cd"; -$fa-var-life-ring: "\f1cd"; -$fa-var-life-saver: "\f1cd"; -$fa-var-lightbulb-o: "\f0eb"; -$fa-var-line-chart: "\f201"; -$fa-var-link: "\f0c1"; -$fa-var-linkedin: "\f0e1"; -$fa-var-linkedin-square: "\f08c"; -$fa-var-linux: "\f17c"; -$fa-var-list: "\f03a"; -$fa-var-list-alt: "\f022"; -$fa-var-list-ol: "\f0cb"; -$fa-var-list-ul: "\f0ca"; -$fa-var-location-arrow: "\f124"; -$fa-var-lock: "\f023"; -$fa-var-long-arrow-down: "\f175"; -$fa-var-long-arrow-left: "\f177"; -$fa-var-long-arrow-right: "\f178"; -$fa-var-long-arrow-up: "\f176"; -$fa-var-low-vision: "\f2a8"; -$fa-var-magic: "\f0d0"; -$fa-var-magnet: "\f076"; -$fa-var-mail-forward: "\f064"; -$fa-var-mail-reply: "\f112"; -$fa-var-mail-reply-all: "\f122"; -$fa-var-male: "\f183"; -$fa-var-map: "\f279"; -$fa-var-map-marker: "\f041"; -$fa-var-map-o: "\f278"; -$fa-var-map-pin: "\f276"; -$fa-var-map-signs: "\f277"; -$fa-var-mars: "\f222"; -$fa-var-mars-double: "\f227"; -$fa-var-mars-stroke: "\f229"; -$fa-var-mars-stroke-h: "\f22b"; -$fa-var-mars-stroke-v: "\f22a"; -$fa-var-maxcdn: "\f136"; -$fa-var-meanpath: "\f20c"; -$fa-var-medium: "\f23a"; -$fa-var-medkit: "\f0fa"; -$fa-var-meh-o: "\f11a"; -$fa-var-mercury: "\f223"; -$fa-var-microphone: "\f130"; -$fa-var-microphone-slash: "\f131"; -$fa-var-minus: "\f068"; -$fa-var-minus-circle: "\f056"; -$fa-var-minus-square: "\f146"; -$fa-var-minus-square-o: "\f147"; -$fa-var-mixcloud: "\f289"; -$fa-var-mobile: "\f10b"; -$fa-var-mobile-phone: "\f10b"; -$fa-var-modx: "\f285"; -$fa-var-money: "\f0d6"; -$fa-var-moon-o: "\f186"; -$fa-var-mortar-board: "\f19d"; -$fa-var-motorcycle: "\f21c"; -$fa-var-mouse-pointer: "\f245"; -$fa-var-music: "\f001"; -$fa-var-navicon: "\f0c9"; -$fa-var-neuter: "\f22c"; -$fa-var-newspaper-o: "\f1ea"; -$fa-var-object-group: "\f247"; -$fa-var-object-ungroup: "\f248"; -$fa-var-odnoklassniki: "\f263"; -$fa-var-odnoklassniki-square: "\f264"; -$fa-var-opencart: "\f23d"; -$fa-var-openid: "\f19b"; -$fa-var-opera: "\f26a"; -$fa-var-optin-monster: "\f23c"; -$fa-var-outdent: "\f03b"; -$fa-var-pagelines: "\f18c"; -$fa-var-paint-brush: "\f1fc"; -$fa-var-paper-plane: "\f1d8"; -$fa-var-paper-plane-o: "\f1d9"; -$fa-var-paperclip: "\f0c6"; -$fa-var-paragraph: "\f1dd"; -$fa-var-paste: "\f0ea"; -$fa-var-pause: "\f04c"; -$fa-var-pause-circle: "\f28b"; -$fa-var-pause-circle-o: "\f28c"; -$fa-var-paw: "\f1b0"; -$fa-var-paypal: "\f1ed"; -$fa-var-pencil: "\f040"; -$fa-var-pencil-square: "\f14b"; -$fa-var-pencil-square-o: "\f044"; -$fa-var-percent: "\f295"; -$fa-var-phone: "\f095"; -$fa-var-phone-square: "\f098"; -$fa-var-photo: "\f03e"; -$fa-var-picture-o: "\f03e"; -$fa-var-pie-chart: "\f200"; -$fa-var-pied-piper: "\f1a7"; -$fa-var-pied-piper-alt: "\f1a8"; -$fa-var-pinterest: "\f0d2"; -$fa-var-pinterest-p: "\f231"; -$fa-var-pinterest-square: "\f0d3"; -$fa-var-plane: "\f072"; -$fa-var-play: "\f04b"; -$fa-var-play-circle: "\f144"; -$fa-var-play-circle-o: "\f01d"; -$fa-var-plug: "\f1e6"; -$fa-var-plus: "\f067"; -$fa-var-plus-circle: "\f055"; -$fa-var-plus-square: "\f0fe"; -$fa-var-plus-square-o: "\f196"; -$fa-var-power-off: "\f011"; -$fa-var-print: "\f02f"; -$fa-var-product-hunt: "\f288"; -$fa-var-puzzle-piece: "\f12e"; -$fa-var-qq: "\f1d6"; -$fa-var-qrcode: "\f029"; -$fa-var-question: "\f128"; -$fa-var-question-circle: "\f059"; -$fa-var-question-circle-o: "\f29c"; -$fa-var-quote-left: "\f10d"; -$fa-var-quote-right: "\f10e"; -$fa-var-ra: "\f1d0"; -$fa-var-random: "\f074"; -$fa-var-rebel: "\f1d0"; -$fa-var-recycle: "\f1b8"; -$fa-var-reddit: "\f1a1"; -$fa-var-reddit-alien: "\f281"; -$fa-var-reddit-square: "\f1a2"; -$fa-var-refresh: "\f021"; -$fa-var-registered: "\f25d"; -$fa-var-remove: "\f00d"; -$fa-var-renren: "\f18b"; -$fa-var-reorder: "\f0c9"; -$fa-var-repeat: "\f01e"; -$fa-var-reply: "\f112"; -$fa-var-reply-all: "\f122"; -$fa-var-retweet: "\f079"; -$fa-var-rmb: "\f157"; -$fa-var-road: "\f018"; -$fa-var-rocket: "\f135"; -$fa-var-rotate-left: "\f0e2"; -$fa-var-rotate-right: "\f01e"; -$fa-var-rouble: "\f158"; -$fa-var-rss: "\f09e"; -$fa-var-rss-square: "\f143"; -$fa-var-rub: "\f158"; -$fa-var-ruble: "\f158"; -$fa-var-rupee: "\f156"; -$fa-var-safari: "\f267"; -$fa-var-save: "\f0c7"; -$fa-var-scissors: "\f0c4"; -$fa-var-scribd: "\f28a"; -$fa-var-search: "\f002"; -$fa-var-search-minus: "\f010"; -$fa-var-search-plus: "\f00e"; -$fa-var-sellsy: "\f213"; -$fa-var-send: "\f1d8"; -$fa-var-send-o: "\f1d9"; -$fa-var-server: "\f233"; -$fa-var-share: "\f064"; -$fa-var-share-alt: "\f1e0"; -$fa-var-share-alt-square: "\f1e1"; -$fa-var-share-square: "\f14d"; -$fa-var-share-square-o: "\f045"; -$fa-var-shekel: "\f20b"; -$fa-var-sheqel: "\f20b"; -$fa-var-shield: "\f132"; -$fa-var-ship: "\f21a"; -$fa-var-shirtsinbulk: "\f214"; -$fa-var-shopping-bag: "\f290"; -$fa-var-shopping-basket: "\f291"; -$fa-var-shopping-cart: "\f07a"; -$fa-var-sign-in: "\f090"; -$fa-var-sign-language: "\f2a7"; -$fa-var-sign-out: "\f08b"; -$fa-var-signal: "\f012"; -$fa-var-signing: "\f2a7"; -$fa-var-simplybuilt: "\f215"; -$fa-var-sitemap: "\f0e8"; -$fa-var-skyatlas: "\f216"; -$fa-var-skype: "\f17e"; -$fa-var-slack: "\f198"; -$fa-var-sliders: "\f1de"; -$fa-var-slideshare: "\f1e7"; -$fa-var-smile-o: "\f118"; -$fa-var-snapchat: "\f2ab"; -$fa-var-snapchat-ghost: "\f2ac"; -$fa-var-snapchat-square: "\f2ad"; -$fa-var-soccer-ball-o: "\f1e3"; -$fa-var-sort: "\f0dc"; -$fa-var-sort-alpha-asc: "\f15d"; -$fa-var-sort-alpha-desc: "\f15e"; -$fa-var-sort-amount-asc: "\f160"; -$fa-var-sort-amount-desc: "\f161"; -$fa-var-sort-asc: "\f0de"; -$fa-var-sort-desc: "\f0dd"; -$fa-var-sort-down: "\f0dd"; -$fa-var-sort-numeric-asc: "\f162"; -$fa-var-sort-numeric-desc: "\f163"; -$fa-var-sort-up: "\f0de"; -$fa-var-soundcloud: "\f1be"; -$fa-var-space-shuttle: "\f197"; -$fa-var-spinner: "\f110"; -$fa-var-spoon: "\f1b1"; -$fa-var-spotify: "\f1bc"; -$fa-var-square: "\f0c8"; -$fa-var-square-o: "\f096"; -$fa-var-stack-exchange: "\f18d"; -$fa-var-stack-overflow: "\f16c"; -$fa-var-star: "\f005"; -$fa-var-star-half: "\f089"; -$fa-var-star-half-empty: "\f123"; -$fa-var-star-half-full: "\f123"; -$fa-var-star-half-o: "\f123"; -$fa-var-star-o: "\f006"; -$fa-var-steam: "\f1b6"; -$fa-var-steam-square: "\f1b7"; -$fa-var-step-backward: "\f048"; -$fa-var-step-forward: "\f051"; -$fa-var-stethoscope: "\f0f1"; -$fa-var-sticky-note: "\f249"; -$fa-var-sticky-note-o: "\f24a"; -$fa-var-stop: "\f04d"; -$fa-var-stop-circle: "\f28d"; -$fa-var-stop-circle-o: "\f28e"; -$fa-var-street-view: "\f21d"; -$fa-var-strikethrough: "\f0cc"; -$fa-var-stumbleupon: "\f1a4"; -$fa-var-stumbleupon-circle: "\f1a3"; -$fa-var-subscript: "\f12c"; -$fa-var-subway: "\f239"; -$fa-var-suitcase: "\f0f2"; -$fa-var-sun-o: "\f185"; -$fa-var-superscript: "\f12b"; -$fa-var-support: "\f1cd"; -$fa-var-table: "\f0ce"; -$fa-var-tablet: "\f10a"; -$fa-var-tachometer: "\f0e4"; -$fa-var-tag: "\f02b"; -$fa-var-tags: "\f02c"; -$fa-var-tasks: "\f0ae"; -$fa-var-taxi: "\f1ba"; -$fa-var-television: "\f26c"; -$fa-var-tencent-weibo: "\f1d5"; -$fa-var-terminal: "\f120"; -$fa-var-text-height: "\f034"; -$fa-var-text-width: "\f035"; -$fa-var-th: "\f00a"; -$fa-var-th-large: "\f009"; -$fa-var-th-list: "\f00b"; -$fa-var-thumb-tack: "\f08d"; -$fa-var-thumbs-down: "\f165"; -$fa-var-thumbs-o-down: "\f088"; -$fa-var-thumbs-o-up: "\f087"; -$fa-var-thumbs-up: "\f164"; -$fa-var-ticket: "\f145"; -$fa-var-times: "\f00d"; -$fa-var-times-circle: "\f057"; -$fa-var-times-circle-o: "\f05c"; -$fa-var-tint: "\f043"; -$fa-var-toggle-down: "\f150"; -$fa-var-toggle-left: "\f191"; -$fa-var-toggle-off: "\f204"; -$fa-var-toggle-on: "\f205"; -$fa-var-toggle-right: "\f152"; -$fa-var-toggle-up: "\f151"; -$fa-var-trademark: "\f25c"; -$fa-var-train: "\f238"; -$fa-var-transgender: "\f224"; -$fa-var-transgender-alt: "\f225"; -$fa-var-trash: "\f1f8"; -$fa-var-trash-o: "\f014"; -$fa-var-tree: "\f1bb"; -$fa-var-trello: "\f181"; -$fa-var-tripadvisor: "\f262"; -$fa-var-trophy: "\f091"; -$fa-var-truck: "\f0d1"; -$fa-var-try: "\f195"; -$fa-var-tty: "\f1e4"; -$fa-var-tumblr: "\f173"; -$fa-var-tumblr-square: "\f174"; -$fa-var-turkish-lira: "\f195"; -$fa-var-tv: "\f26c"; -$fa-var-twitch: "\f1e8"; -$fa-var-twitter: "\f099"; -$fa-var-twitter-square: "\f081"; -$fa-var-umbrella: "\f0e9"; -$fa-var-underline: "\f0cd"; -$fa-var-undo: "\f0e2"; -$fa-var-universal-access: "\f29a"; -$fa-var-university: "\f19c"; -$fa-var-unlink: "\f127"; -$fa-var-unlock: "\f09c"; -$fa-var-unlock-alt: "\f13e"; -$fa-var-unsorted: "\f0dc"; -$fa-var-upload: "\f093"; -$fa-var-usb: "\f287"; -$fa-var-usd: "\f155"; -$fa-var-user: "\f007"; -$fa-var-user-md: "\f0f0"; -$fa-var-user-plus: "\f234"; -$fa-var-user-secret: "\f21b"; -$fa-var-user-times: "\f235"; -$fa-var-users: "\f0c0"; -$fa-var-venus: "\f221"; -$fa-var-venus-double: "\f226"; -$fa-var-venus-mars: "\f228"; -$fa-var-viacoin: "\f237"; -$fa-var-viadeo: "\f2a9"; -$fa-var-viadeo-square: "\f2aa"; -$fa-var-video-camera: "\f03d"; -$fa-var-vimeo: "\f27d"; -$fa-var-vimeo-square: "\f194"; -$fa-var-vine: "\f1ca"; -$fa-var-vk: "\f189"; -$fa-var-volume-control-phone: "\f2a0"; -$fa-var-volume-down: "\f027"; -$fa-var-volume-off: "\f026"; -$fa-var-volume-up: "\f028"; -$fa-var-warning: "\f071"; -$fa-var-wechat: "\f1d7"; -$fa-var-weibo: "\f18a"; -$fa-var-weixin: "\f1d7"; -$fa-var-whatsapp: "\f232"; -$fa-var-wheelchair: "\f193"; -$fa-var-wheelchair-alt: "\f29b"; -$fa-var-wifi: "\f1eb"; -$fa-var-wikipedia-w: "\f266"; -$fa-var-windows: "\f17a"; -$fa-var-won: "\f159"; -$fa-var-wordpress: "\f19a"; -$fa-var-wpbeginner: "\f297"; -$fa-var-wpforms: "\f298"; -$fa-var-wrench: "\f0ad"; -$fa-var-xing: "\f168"; -$fa-var-xing-square: "\f169"; -$fa-var-y-combinator: "\f23b"; -$fa-var-y-combinator-square: "\f1d4"; -$fa-var-yahoo: "\f19e"; -$fa-var-yc: "\f23b"; -$fa-var-yc-square: "\f1d4"; -$fa-var-yelp: "\f1e9"; -$fa-var-yen: "\f157"; -$fa-var-youtube: "\f167"; -$fa-var-youtube-play: "\f16a"; -$fa-var-youtube-square: "\f166"; - diff --git a/iam-login-service/src/main/webapp/resources/font-awesome/scss/font-awesome.scss b/iam-login-service/src/main/webapp/resources/font-awesome/scss/font-awesome.scss deleted file mode 100644 index a19d664c3..000000000 --- a/iam-login-service/src/main/webapp/resources/font-awesome/scss/font-awesome.scss +++ /dev/null @@ -1,18 +0,0 @@ -/*! - * Font Awesome 4.6.1 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */ - -@import "variables"; -@import "mixins"; -@import "path"; -@import "core"; -@import "larger"; -@import "fixed-width"; -@import "list"; -@import "bordered-pulled"; -@import "animated"; -@import "rotated-flipped"; -@import "stacked"; -@import "icons"; -@import "screen-reader"; diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/registration/registration.html b/iam-login-service/src/main/webapp/resources/iam/apps/registration/registration.html index 9f960a48c..094f02132 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/registration/registration.html +++ b/iam-login-service/src/main/webapp/resources/iam/apps/registration/registration.html @@ -79,8 +79,10 @@

- + + + Please writes a reason for your registration +
diff --git a/iam-login-service/src/main/webapp/resources/iam/css/iam.css b/iam-login-service/src/main/webapp/resources/iam/css/iam.css index 767aac871..0745c2120 100644 --- a/iam-login-service/src/main/webapp/resources/iam/css/iam.css +++ b/iam-login-service/src/main/webapp/resources/iam/css/iam.css @@ -183,7 +183,7 @@ body.skin-blue { .version-info{ text-align: center; position: absolute; - color: #4b646f; + color: #b8c7ce; background: #1a2226; bottom: 0; @@ -191,4 +191,14 @@ body.skin-blue { width: 230px; height: 50px; font-size: 12px; +} + +.subjectDn { + font-weight: 600; +} + +.issuerDn { + margin-top: .2em; + font-size: smaller; + color: #4b646f; } \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/groups/user.groups.component.js b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/groups/user.groups.component.js index 821b26bb4..7d4e6ede6 100644 --- a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/groups/user.groups.component.js +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/groups/user.groups.component.js @@ -11,12 +11,12 @@ self.isVoAdmin = function() { return self.userCtrl.isVoAdmin(); }; - self.handleSuccess = function() { + self.handleSuccess = function(msg) { self.enabled = true; - self.userCtrl.loadUser().then(function(user) { + self.userCtrl.loadUser().then(function() { toaster.pop({ type: 'success', - body: `User '${user.name.formatted}' groups updated succesfully` + body: msg }); }); }; @@ -48,7 +48,9 @@ .removeUserFromGroup( group.value, self.user.id, self.user.meta.location, self.user.name.formatted) - .then(self.handleSuccess) + .then(function(){ + self.handleSuccess(`User removed from group '${group.display}'`); + }) .catch(self.userCtrl.handleError); }); diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/oidc/oidc-id.add.dialog.html b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/oidc/oidc-id.add.dialog.html new file mode 100644 index 000000000..b2598fd91 --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/oidc/oidc-id.add.dialog.html @@ -0,0 +1,44 @@ + + + + \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/oidc/oidc-id.remove.dialog.html b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/oidc/oidc-id.remove.dialog.html new file mode 100644 index 000000000..d5233e45d --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/oidc/oidc-id.remove.dialog.html @@ -0,0 +1,25 @@ + + + +
+ + \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/oidc/user.oidc.component.html b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/oidc/user.oidc.component.html index c34f21f92..e48539d34 100644 --- a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/oidc/user.oidc.component.html +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/oidc/user.oidc.component.html @@ -21,10 +21,10 @@

OpenID-Connect accounts

{{ oidcId.subject }}
- -
@@ -35,11 +35,11 @@

OpenID-Connect accounts

diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/oidc/user.oidc.component.js b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/oidc/user.oidc.component.js index e40de6654..4e0501213 100644 --- a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/oidc/user.oidc.component.js +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/oidc/user.oidc.component.js @@ -1,6 +1,76 @@ -(function() { +(function () { 'use strict'; + function RemoveOidcController($uibModalInstance, scimFactory, user, account) { + var self = this; + + self.user = user; + self.enabled = true; + self.error = undefined; + self.account = account; + + self.doRemove = function () { + self.error = undefined; + self.enabled = false; + scimFactory.removeOpenIDAccount(self.user.id, self.account).then(function (response) { + $uibModalInstance.close('OpenID Connect account removed'); + self.enabled = true; + }).catch(function (error) { + console.error(error); + self.error = error; + self.enabled = true; + }); + }; + + self.cancel = function () { + $uibModalInstance.dismiss('Dismissed'); + }; + } + + + function AddOidcController($uibModalInstance, scimFactory, user) { + var self = this; + + self.user = user; + self.enabled = true; + self.error = undefined; + + self.reset = function () { + self.account = { + "issuer": "", + "subject": "" + }; + self.enabled = true; + }; + + self.reset(); + + self.doAdd = function () { + self.error = undefined; + self.enabled = false; + scimFactory.addOpenIDAccount(self.user.id, self.account).then(function (response) { + $uibModalInstance.close('OpenID Connect account added'); + self.enabled = true; + }).catch(function (error) { + console.error(error); + self.error = error; + self.enabled = true; + }); + }; + + self.cancel = function () { + $uibModalInstance.dismiss('Dismissed'); + }; + + self.reset = function () { + self.account = { + "issuer": "", + "subject": "" + }; + self.enabled = true; + }; + } + function LinkOidcController(AccountLinkingService, $uibModalInstance, action, account) { var self = this; @@ -10,51 +80,48 @@ self.account = account; self.enabled = true; - self.doLink = function() { + self.doLink = function () { self.enabled = false; document.getElementById("link-account-form").submit(); }; - self.doUnlink = function() { + self.doUnlink = function () { self.enabled = false; - AccountLinkingService.unlinkOidcAccount(self.account).then(function(response){ - $uibModalInstance.close(response); - }).catch(function(error){ + AccountLinkingService.unlinkOidcAccount(self.account).then(function (response) { + $uibModalInstance.close("Google account unlinked"); + }).catch(function (error) { console.error(error); }); }; - self.cancel = function() { $uibModalInstance.dismiss('Dismissed'); }; + self.cancel = function () { + $uibModalInstance.dismiss('Dismissed'); + }; } function UserOidcController($uibModal, toaster, ModalService, scimFactory) { var self = this; - self.indigoUser = function() { + self.indigoUser = function () { return self.user['urn:indigo-dc:scim:schemas:IndigoUser']; }; - self.isMe = function() { return self.userCtrl.isMe(); }; - - self.isVoAdmin = function() { return self.userCtrl.isVoAdmin(); }; - - self.$onInit = function() { + self.$onInit = function () { console.log('UserOidcController onInit'); self.enabled = true; }; - self.handleSuccess = function(user) { + self.handleSuccess = function (msg) { self.enabled = true; - self.userCtrl.loadUser().then(function(user) { + self.userCtrl.loadUser().then(function () { toaster.pop({ type: 'success', - body: - `User '${user.name.formatted}' OpenID-Connect accounts updated succesfully` + body: msg }); }); }; - self.hasOidcIds = function() { + self.hasOidcIds = function () { var oidcIds = self.getOidcIds(); if (oidcIds) { @@ -64,7 +131,7 @@ return false; }; - self.getOidcIds = function() { + self.getOidcIds = function () { if (self.indigoUser()) { return self.indigoUser().oidcIds; } @@ -72,40 +139,52 @@ return undefined; }; - self.openAddOidcAccountDialog = function() { + self.openAddOidcAccountDialog = function () { var modalInstance = $uibModal.open({ - templateUrl: '/resources/iam/template/dashboard/user/addoidc.html', - controller: 'AddOIDCAccountController', - controllerAs: 'addOidcCtrl', - resolve: {user: function() { return self.user; }} + templateUrl: '/resources/iam/js/dashboard-app/components/user/oidc/oidc-id.add.dialog.html', + controller: AddOidcController, + controllerAs: '$ctrl', + resolve: { + user: function () { + return self.user; + } + } }); modalInstance.result.then(self.handleSuccess); }; - self.openLinkOidcAccountDialog = function() { + self.openLinkOidcAccountDialog = function () { var modalInstance = $uibModal.open({ - templateUrl: - '/resources/iam/js/dashboard-app/components/common/user.linking.dialog.html', + templateUrl: '/resources/iam/js/dashboard-app/components/common/user.linking.dialog.html', controller: LinkOidcController, controllerAs: '$ctrl', - resolve: {action: function() { return 'link'; }, account: undefined} + resolve: { + action: function () { + return 'link'; + }, + account: undefined + } }); modalInstance.result.then(self.handleSuccess); }; - self.openUnlinkOidcAccountDialog = function(oidcId) { + self.openUnlinkOidcAccountDialog = function (oidcId) { var modalInstance = $uibModal.open({ - templateUrl: - '/resources/iam/js/dashboard-app/components/common/user.linking.dialog.html', + templateUrl: '/resources/iam/js/dashboard-app/components/common/user.linking.dialog.html', controller: LinkOidcController, controllerAs: '$ctrl', resolve: { - action: function() { return 'unlink'; }, - account: function() { - return { iss: oidcId.issuer, sub: oidcId.subject } + action: function () { + return 'unlink'; + }, + account: function () { + return { + iss: oidcId.issuer, + sub: oidcId.subject + } } } }); @@ -115,33 +194,32 @@ - self.openRemoveOidcAccountDialog = function(oidcId) { - var account = `${oidcId.issuer} : ${oidcId.subject}`; - - var modalOptions = { - closeButtonText: 'Cancel', - actionButtonText: `Remove OpenID-Connect Account'`, - headerText: - `Remove OpenID-Connect Account from user '${self.user.name.formatted}'`, - bodyText: - `Are you sure you want to remove the following Open ID Account from user '${self.user.name.formatted}'?`, - bodyDetail: account - }; + self.openRemoveOidcAccountDialog = function (oidcId) { - ModalService.showModal({}, modalOptions).then(function() { - scimFactory - .removeOpenIDAccount(self.user.id, oidcId.issuer, oidcId.subject) - .then(self.handleSuccess) - .catch(self.userCtrl.handleError); + var modalInstance = $uibModal.open({ + templateUrl: '/resources/iam/js/dashboard-app/components/user/oidc/oidc-id.remove.dialog.html', + controller: RemoveOidcController, + controllerAs: '$ctrl', + resolve: { + user: function () { + return self.user; + }, + account: oidcId + } }); + + modalInstance.result.then(self.handleSuccess); }; } angular.module('dashboardApp').component('userOidc', { - require: {userCtrl: '^user'}, - bindings: {user: '='}, - templateUrl: - '/resources/iam/js/dashboard-app/components/user/oidc/user.oidc.component.html', + require: { + userCtrl: '^user' + }, + bindings: { + user: '=' + }, + templateUrl: '/resources/iam/js/dashboard-app/components/user/oidc/user.oidc.component.html', controller: [ '$uibModal', 'toaster', 'ModalService', 'scimFactory', UserOidcController ] diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/saml/saml-id.add.dialog.html b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/saml/saml-id.add.dialog.html new file mode 100644 index 000000000..9160045d4 --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/saml/saml-id.add.dialog.html @@ -0,0 +1,57 @@ + + + + \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/saml/saml-id.remove.dialog.html b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/saml/saml-id.remove.dialog.html new file mode 100644 index 000000000..989ace68c --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/saml/saml-id.remove.dialog.html @@ -0,0 +1,28 @@ + + + + + + \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/saml/saml-id.unlink.dialog.html b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/saml/saml-id.unlink.dialog.html new file mode 100644 index 000000000..85c6565c2 --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/saml/saml-id.unlink.dialog.html @@ -0,0 +1,32 @@ + + + + + + \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/saml/user.saml.component.html b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/saml/user.saml.component.html index a82f55ee5..d1ee2397c 100644 --- a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/saml/user.saml.component.html +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/saml/user.saml.component.html @@ -5,29 +5,33 @@

Saml accounts

- +
No SAML account found
+
- - + + + + +
Identity Provider IDUser IDIdP Entity IDAttributeAttribute Value
{{ samlId.idpId }}{{ samlId.attributeId }} {{ samlId.userId }}
- -
@@ -36,12 +40,13 @@

Saml accounts

+
diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/saml/user.saml.component.js b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/saml/user.saml.component.js index 13988db66..457974be0 100644 --- a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/saml/user.saml.component.js +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/saml/user.saml.component.js @@ -1,6 +1,68 @@ -(function() { +(function () { 'use strict'; + function RemoveSamlIdController($uibModalInstance, scimFactory, user, samlId) { + var self = this; + + self.error = undefined; + self.user = user; + self.samlId = samlId; + self.enabled = true; + + + self.cancel = function () { + $uibModalInstance.dismiss("Cancel"); + }; + + self.doRemove = function () { + self.enabled = false; + self.error = undefined; + scimFactory.removeSamlId(self.user.id, self.samlId).then(function (response) { + $uibModalInstance.close(`SAML account removed`); + self.enabled = true; + }).catch(function (error) { + console.error(error); + self.error = error; + self.enabled = true; + }); + }; + } + + function AddSamlIdController($uibModalInstance, scimFactory, user) { + var self = this; + + self.error = undefined; + self.user = user; + + self.reset = function () { + self.samlId = { + "idpId": "", + "attributeId": "", + "userId": "" + }; + self.enabled = true; + }; + + self.reset(); + + self.cancel = function () { + $uibModalInstance.dismiss("Cancel"); + }; + + self.doAdd = function () { + self.error = undefined; + self.enabled = false; + scimFactory.addSamlId(self.user.id, self.samlId).then(function (response) { + $uibModalInstance.close(`SAML account added`); + self.enabled = true; + }).catch(function (error) { + console.error(error); + self.error = error; + self.enabled = true; + }); + }; + } + function LinkSamlController(AccountLinkingService, $uibModalInstance, action, account) { var self = this; @@ -9,45 +71,64 @@ self.actionUrl = '/iam/account-linking/SAML'; self.type = 'SAML'; self.account = account; + self.error = undefined; self.doLink = function() { self.enabled = false; document.getElementById("link-account-form").submit(); }; - self.doUnlink = function() { + + self.doUnlink = function () { self.enabled = false; - AccountLinkingService.unlinkSamlAccount(self.account).then(function(response){ - $uibModalInstance.close(response); - }).catch(function(error){ + self.error = undefined; + + AccountLinkingService.unlinkSamlAccount({ + iss: self.account.idpId, + attr: self.account.attributeId, + sub: self.account.userId + }).then(function (response) { + $uibModalInstance.close("SAML account unlinked"); + }).catch(function (error) { console.error(error); + + self.error = error; + self.enabled = true; }); }; - self.cancel = function() { $uibModalInstance.dismiss('Dismissed'); }; + self.cancel = function () { + $uibModalInstance.dismiss('Dismissed'); + }; } function IdpSelectionController( - $scope, $http, $uibModalInstance, $window, $timeout) { - $scope.ok = function() { + $scope, $http, $uibModalInstance, $window, $timeout) { + $scope.ok = function () { $window.location.href = '/saml/login?idp=' + $scope.idpSelected.entityId; }; - $scope.cancel = function() { $uibModalInstance.close(); }; + $scope.cancel = function () { + $uibModalInstance.close(); + }; - $scope.reset = function() { + $scope.reset = function () { $scope.idpSelected = null; - $timeout(function() { + $timeout(function () { $window.document.getElementById('idp-selection-input').focus(); }, 0); }; - $scope.lookupIdp = function(val) { + $scope.lookupIdp = function (val) { var result = - $http.get('/saml/idps', {params: {q: val}}).then(function(response) { - return response.data; - }); + $http.get('/saml/idps', { + params: { + q: val + } + }).then(function (response) { + return response.data; + }); return result; }; @@ -56,86 +137,119 @@ function UserSamlController(toaster, $uibModal, ModalService, scimFactory) { var self = this; - self.indigoUser = function() { + self.SAML_ATTRIBUTES = [{ + name: "eduPersonUniqueId", + oid: "urn:oid:1.3.6.1.4.1.5923.1.1.1.13" + }, { + name: "eduPersonTargetedId", + oid: "urn:oid:1.3.6.1.4.1.5923.1.1.1.10" + }, { + name: "eduPersonPrincipalName", + oid: "urn:oid:1.3.6.1.4.1.5923.1.1.1.6" + }, { + name: "eduPersonOrcid", + oid: "urn:oid:1.3.6.1.4.1.5923.1.1.1.16" + }, { + name: "employeeNumber", + oid: "urn:oid:2.16.840.1.113730.3.1.3" + }, { + name: "spidCode", + oid: "spidCode" + }]; + + self.SAML_NAME_TO_OID = {}; + self.SAML_OID_TO_NAME = {}; + + for (let v in self.SAML_ATTRIBUTES) { + self.SAML_NAME_TO_OID[v.name] = v.oid; + self.SAML_OID_TO_NAME[v.oid] = v.name; + } + + self.samlAttributeNameToOid = function (name) { + return self.SAML_NAME_TO_OID[name]; + }; + + self.samlAttributeOidToName = function (oid) { + return self.SAML_OID_TO_NAME[oid]; + }; + + self.indigoUser = function () { return self.user['urn:indigo-dc:scim:schemas:IndigoUser']; }; - self.$onInit = function() { + self.$onInit = function () { console.log('UserSamlController onInit'); self.enabled = true; }; - self.isVoAdmin = function() { return self.userCtrl.isVoAdmin(); }; - - self.isMe = function() { return self.userCtrl.isMe(); }; - - self.handleSuccess = function(user) { + self.handleSuccess = function (msg) { self.enabled = true; - self.userCtrl.loadUser().then(function(user) { + self.userCtrl.loadUser().then(function () { toaster.pop({ type: 'success', - body: - `User '${user.name.formatted}' SAML accounts updated succesfully` + body: msg }); }); }; - - self.openLinkSamlAccountDialog = function() { + + self.openLinkSamlAccountDialog = function () { var modalInstance = $uibModal.open({ - templateUrl: - '/resources/iam/js/dashboard-app/components/common/user.linking.dialog.html', + templateUrl: '/resources/iam/js/dashboard-app/components/common/user.linking.dialog.html', controller: LinkSamlController, controllerAs: '$ctrl', - resolve: {action: function() { return 'link'; }, account: undefined} + resolve: { + action: function () { + return 'link'; + }, + account: undefined + } }); modalInstance.result.then(self.handleSuccess); }; - self.openAddSamlAccountDialog = function() { + self.openAddSamlAccountDialog = function () { var modalInstance = $uibModal.open({ - templateUrl: - '/resources/iam/template/dashboard/user/addsamlaccount.html', - controller: 'AddSamlAccountController', - controllerAs: 'addSamlAccountCtrl', - resolve: {user: function() { return self.user; }} + templateUrl: '/resources/iam/js/dashboard-app/components/user/saml/saml-id.add.dialog.html', + controller: AddSamlIdController, + controllerAs: '$ctrl', + resolve: { + user: function () { + return self.user; + } + } }); modalInstance.result.then(self.handleSuccess); }; - self.openRemoveSamlAccountDialog = function(samlId) { - var account = `${samlId.idpId} : ${samlId.userId}`; - - var modalOptions = { - closeButtonText: 'Cancel', - actionButtonText: 'Remove SAML Account', - headerText: - `Remove SAML account from user '${self.user.name.formatted}'`, - bodyText: - `Are you sure you want to remove the following SAML Account from user '${self.user.name.formatted}'?`, - bodyDetail: account - }; - - ModalService.showModal({}, modalOptions).then(function() { - scimFactory.removeSamlId(self.user.id, samlId) - .then(self.handleSuccess) - .catch(self.userCtrl.handleError); + self.openRemoveSamlAccountDialog = function (samlId) { + var modalInstance = $uibModal.open({ + templateUrl: '/resources/iam/js/dashboard-app/components/user/saml/saml-id.remove.dialog.html', + controller: RemoveSamlIdController, + controllerAs: '$ctrl', + resolve: { + user: function () { + return self.user; + }, + samlId: samlId + } }); + + modalInstance.result.then(self.handleSuccess); }; - self.openUnlinkSamlAccountDialog = function(samlId) { + self.openUnlinkSamlAccountDialog = function (samlId) { var modalInstance = $uibModal.open({ - templateUrl: - '/resources/iam/js/dashboard-app/components/common/user.linking.dialog.html', + templateUrl: '/resources/iam/js/dashboard-app/components/user/saml/saml-id.unlink.dialog.html', controller: LinkSamlController, controllerAs: '$ctrl', resolve: { - action: function() { return 'unlink'; }, - account: function() { - return { iss: samlId.idpId, sub: samlId.userId } - } + action: function () { + return 'unlink'; + }, + account: samlId } }); @@ -143,7 +257,7 @@ }; - self.hasSamlIds = function() { + self.hasSamlIds = function () { var samlIds = self.getSamlIds(); if (samlIds) { @@ -153,7 +267,7 @@ return false; }; - self.getSamlIds = function() { + self.getSamlIds = function () { if (self.indigoUser()) { return self.indigoUser().samlIds; } @@ -163,12 +277,15 @@ } angular.module('dashboardApp').component('userSaml', { - require: {userCtrl: '^user'}, - bindings: {user: '='}, - templateUrl: - '/resources/iam/js/dashboard-app/components/user/saml/user.saml.component.html', + require: { + userCtrl: '^user' + }, + bindings: { + user: '=' + }, + templateUrl: '/resources/iam/js/dashboard-app/components/user/saml/user.saml.component.html', controller: [ 'toaster', '$uibModal', 'ModalService', 'scimFactory', UserSamlController ] }); -})(); \ No newline at end of file +})(); diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/user.component.html b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/user.component.html index eca94cca2..9e1927c7b 100644 --- a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/user.component.html +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/user.component.html @@ -45,6 +45,7 @@

+ diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/user.component.js b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/user.component.js index 019b3561d..e1f13e103 100644 --- a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/user.component.js +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/user.component.js @@ -12,10 +12,26 @@ $timeout(self.showAccountLinkingFeedback, 0); }; + self.accountLinkingEnabled = getAccountLinkingEnabled(); + self.isVoAdmin = function() { return Utils.isAdmin(); }; self.userIsVoAdmin = function() { return Utils.userIsVoAdmin(self.user); }; + self.isMe = function() { return Utils.isMe(self.user.id); }; + + self.canManageLinkedAccounts = function() { + if (!self.accountLinkingEnabled){ + return self.isVoAdmin(); + } + + return self.isVoAdmin() && !self.isMe(); + }; + + self.canLinkAccounts = function() { + return self.accountLinkingEnabled && self.isMe(); + }; + self.showAccountLinkingFeedback = function() { if (_accountLinkingError){ toaster.pop({type: 'error', body: _accountLinkingError}); @@ -28,8 +44,6 @@ } }; - self.isMe = function() { return Utils.isMe(self.user.id); }; - self.handleError = function(error) { console.error(error); toaster.pop({type: 'error', body: error}); diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/x509/cert.add.dialog.html b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/x509/cert.add.dialog.html new file mode 100644 index 000000000..b9727ea27 --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/x509/cert.add.dialog.html @@ -0,0 +1,49 @@ + + + + + \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/x509/cert.link.dialog.html b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/x509/cert.link.dialog.html new file mode 100644 index 000000000..40ef54768 --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/x509/cert.link.dialog.html @@ -0,0 +1,55 @@ + + + \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/x509/cert.remove.dialog.html b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/x509/cert.remove.dialog.html new file mode 100644 index 000000000..8cad0877d --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/x509/cert.remove.dialog.html @@ -0,0 +1,27 @@ + + + + + \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/x509/user.x509.component.html b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/x509/user.x509.component.html new file mode 100644 index 000000000..ca5e962b3 --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/x509/user.x509.component.html @@ -0,0 +1,51 @@ + +
+
+

X.509 certificates

+ +
+ +
+
No certificates found
+ +
+ + + + + + + + + + + + + + + + + +
Subject & IssuerLast modified
+
{{ cert.subjectDn }}
+
{{ cert.issuerDn }}
+
{{ cert.lastModified | relativeDate }} +
+ + +
+
+
+ +
\ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/x509/user.x509.component.js b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/x509/user.x509.component.js new file mode 100644 index 000000000..a192cf969 --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/components/user/x509/user.x509.component.js @@ -0,0 +1,234 @@ +(function () { + 'use strict'; + + function AddRemoveCertificateController($scope, scimFactory, $uibModalInstance, action, cert, user, successHandler) { + var self = this; + self.enabled = true; + self.action = action; + self.cert = cert; + self.user = user; + self.error = undefined; + self.successHandler = successHandler; + + self.certVal = {}; + + self.doAdd = function () { + self.error = undefined; + self.enabled = false; + scimFactory.addX509Certificate(self.user.id, self.certVal).then( + function (response) { + $uibModalInstance.close(response.data); + self.successHandler(`Certificate added`); + self.enabled = true; + }).catch(function (error) { + console.error(error); + self.error = error; + self.enabled = true; + }); + }; + + self.doRemove = function () { + self.error = undefined; + self.enabled = false; + scimFactory.removeX509Certificate(self.user.id, self.cert) + .then(function (response) { + $uibModalInstance.close(response.data); + self.successHandler(`Certificate ${self.cert.subjectDn} removed`); + self.enabled = true; + }).catch(function (error) { + console.error(error); + self.enabled = true; + self.error = error; + }); + }; + + self.cancel = function () { + $uibModalInstance.dismiss('Dismissed'); + }; + + self.reset = function () { + self.certVal = { + label: "", + primary: false, + pemEncodedCertificate: "", + }; + self.error = undefined; + }; + + self.certLabelValid = function() { + return $scope.certLabel.$dirty && $scope.certLabel.$valid; + }; + } + + function LinkCertificateController(AccountLinkingService, $uibModalInstance, action, cert) { + + var self = this; + + self.enabled = true; + self.action = action; + self.actionUrl = '/iam/account-linking/X509'; + self.cert = cert; + + self.doLink = function () { + self.enabled = false; + document.getElementById("link-certificate-form").submit(); + }; + + self.doUnlink = function () { + self.enabled = false; + AccountLinkingService.unlinkX509Certificate(self.cert).then(function (response) { + $uibModalInstance.close(`Certificate '${self.cert.subjectDn}' unlinked succesfully`); + }).catch(function (error) { + console.error(error); + self.error = error; + self.enabled = true; + }); + }; + + self.cancel = function () { + $uibModalInstance.dismiss('Dismissed'); + }; + } + + function UserX509Controller(toaster, $uibModal, ModalService, scimFactory) { + var self = this; + + self.accountLinkingEnabled = getAccountLinkingEnabled(); + + self.indigoUser = function () { + return self.user['urn:indigo-dc:scim:schemas:IndigoUser']; + }; + + self.$onInit = function () { + console.log('UserX509Controller onInit'); + self.enabled = true; + }; + + self.getCertificates = function () { + if (self.indigoUser()) { + return self.indigoUser().certificates; + } + + return undefined; + }; + + self.getUserCertSubject = function () { + return getUserX509CertficateSubject(); + }; + + self.getUserCertIssuer = function () { + return getUserX509CertficateIssuer(); + }; + + self.hasCertificates = function () { + var certs = self.getCertificates(); + if (certs) { + return certs.length > 0; + } + + return false; + }; + + self.handleSuccess = function (msg) { + self.enabled = true; + self.userCtrl.loadUser().then(function (user) { + toaster.pop({ + type: 'success', + body: msg + }); + }); + }; + + self.openAddCertificateDialog = function () { + var modalInstance = $uibModal.open({ + templateUrl: '/resources/iam/js/dashboard-app/components/user/x509/cert.add.dialog.html', + controller: AddRemoveCertificateController, + controllerAs: '$ctrl', + resolve: { + action: function () { + return 'add'; + }, + user: function () { + return self.user; + }, + cert: undefined, + successHandler: function () { + return self.handleSuccess; + } + } + }); + }; + + self.openRemoveCertificateDialog = function (cert) { + var modalInstance = $uibModal.open({ + templateUrl: '/resources/iam/js/dashboard-app/components/user/x509/cert.remove.dialog.html', + controller: AddRemoveCertificateController, + controllerAs: '$ctrl', + resolve: { + action: function () { + return 'remove'; + }, + user: function () { + return self.user; + }, + cert: function () { + return cert; + }, + successHandler: function () { + return self.handleSuccess; + } + } + }); + }; + + self.openLinkCertificateDialog = function () { + var modalInstance = $uibModal.open({ + templateUrl: '/resources/iam/js/dashboard-app/components/user/x509/cert.link.dialog.html', + controller: LinkCertificateController, + controllerAs: '$ctrl', + resolve: { + action: function () { + return 'link'; + }, + cert: { + subjectDn: self.getUserCertSubject(), + issuerDn: self.getUserCertIssuer() + } + } + }); + + modalInstance.result.then(self.handleSuccess); + }; + + self.openUnlinkCertificateDialog = function (certificate) { + var modalInstance = $uibModal.open({ + templateUrl: '/resources/iam/js/dashboard-app/components/user/x509/cert.link.dialog.html', + controller: LinkCertificateController, + controllerAs: '$ctrl', + resolve: { + action: function () { + return 'unlink'; + }, + cert: function () { + return certificate; + } + } + }); + + modalInstance.result.then(self.handleSuccess); + }; + } + + angular.module('dashboardApp').component('userX509', { + require: { + userCtrl: '^user' + }, + bindings: { + user: '=' + }, + templateUrl: '/resources/iam/js/dashboard-app/components/user/x509/user.x509.component.html', + controller: [ + 'toaster', '$uibModal', 'ModalService', 'scimFactory', UserX509Controller + ] + }); +})(); \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-group.controller.js b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-group.controller.js index ea2b27f7c..fa9b296a9 100644 --- a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-group.controller.js +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-group.controller.js @@ -14,6 +14,8 @@ function AddGroupController($scope, $rootScope, $uibModalInstance, Utils, scimFa addGroupCtrl.addGroup = addGroup; addGroupCtrl.resetGroup = resetGroup; addGroupCtrl.cancel = cancel; + addGroupCtrl.parents = $rootScope.groups; + addGroupCtrl.parentSelected = addGroupCtrl.parents[-1]; function resetGroup() { @@ -30,7 +32,19 @@ function AddGroupController($scope, $rootScope, $uibModalInstance, Utils, scimFa addGroupCtrl.group.schemas = []; addGroupCtrl.group.schemas[0] = "urn:ietf:params:scim:schemas:core:2.0:Group"; + addGroupCtrl.group.schemas[1] = "urn:indigo-dc:scim:schemas:IndigoGroup"; + var parent = addGroupCtrl.parentSelected + if(parent){ + addGroupCtrl.group["urn:indigo-dc:scim:schemas:IndigoGroup"] = { + "parentGroup": { + display: parent.displayName, + value: parent.id, + $ref: parent.meta.location + } + } + } + console.info(addGroupCtrl.group); scimFactory.createGroup(addGroupCtrl.group).then(function(response) { diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-subgroup.controller.js b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-subgroup.controller.js new file mode 100644 index 000000000..5fb7d0c84 --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-subgroup.controller.js @@ -0,0 +1,56 @@ +'use strict'; + +angular.module('dashboardApp').controller('AddSubGroupController', AddSubGroupController); + +AddSubGroupController.$inject = [ '$scope', '$rootScope', '$uibModalInstance', 'Utils', 'scimFactory', 'group' ]; + +function AddSubGroupController($scope, $rootScope, $uibModalInstance, Utils, scimFactory, group) { + + var addSubGroupCtrl = this; + + addSubGroupCtrl.parent = group; + addSubGroupCtrl.addSubGroup = addSubGroup; + addSubGroupCtrl.resetGroup = resetGroup; + addSubGroupCtrl.cancel = cancel; + + addSubGroupCtrl.subgroup = {}; + + function resetGroup() { + + addSubGroupCtrl.subgroup.displayName = ""; + addSubGroupCtrl.enabled = true; + + } + + addSubGroupCtrl.resetGroup(); + + function addSubGroup() { + + addSubGroupCtrl.enabled = false; + + addSubGroupCtrl.subgroup.schemas = ["urn:ietf:params:scim:schemas:core:2.0:Group", "urn:indigo-dc:scim:schemas:IndigoGroup"]; + addSubGroupCtrl.subgroup["urn:indigo-dc:scim:schemas:IndigoGroup"] = { + "parentGroup": { + display: addSubGroupCtrl.parent.displayName, + value: addSubGroupCtrl.parent.id, + $ref: addSubGroupCtrl.parent.meta.location + } + } + + console.info(addSubGroupCtrl.subgroup); + + scimFactory.createGroup(addSubGroupCtrl.subgroup).then(function(response) { + $rootScope.loggedUser.totGroups = $rootScope.loggedUser.totGroups + 1; + $uibModalInstance.close(response.data); + addSubGroupCtrl.enabled = true; + }, function(error) { + console.error('Error creating group', error); + $scope.operationResult = Utils.buildErrorOperationResult(error); + addSubGroupCtrl.enabled = true; + }); + } + + function cancel() { + $uibModalInstance.dismiss("cancel"); + } +} \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-user-group.controller.js b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-user-group.controller.js index bfe2440c0..08f8b98b7 100644 --- a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-user-group.controller.js +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-user-group.controller.js @@ -50,14 +50,16 @@ function AddUserGroupController($scope, $state, $filter, Utils, $q, $uibModalIns addGroupCtrl.enabled = false; var requests = []; + var groupNames = []; angular.forEach(addGroupCtrl.groupsSelected, function(groupToAdd) { + groupNames.push(groupToAdd.displayName); requests.push(scimFactory.addUserToGroup(groupToAdd.id, addGroupCtrl.user)); }); $q.all(requests).then(function(response) { console.log("Added ", addGroupCtrl.groupsSelected); - $uibModalInstance.close(response); + $uibModalInstance.close(`User added to groups: '${groupNames.join(",")}'`); addGroupCtrl.enabled = true; }, function(error) { console.error(error); diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-user-oidc-account.controller.js b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-user-oidc-account.controller.js deleted file mode 100644 index ceeaf6ddc..000000000 --- a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-user-oidc-account.controller.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -angular.module('dashboardApp').controller('AddOIDCAccountController', - AddOIDCAccountController); - -AddOIDCAccountController.$inject = [ '$scope', '$uibModalInstance', - 'scimFactory', '$state', 'Utils', 'user' ]; - -function AddOIDCAccountController($scope, $uibModalInstance, scimFactory, - $state, Utils, user) { - - var addOidcCtrl = this; - addOidcCtrl.user = user; - addOidcCtrl.cancel = cancel; - - addOidcCtrl.addOidcAccount = addOidcAccount; - addOidcCtrl.reset = reset; - - addOidcCtrl.reset(); - - function reset() { - - addOidcCtrl.issuer = ""; - addOidcCtrl.subject = ""; - addOidcCtrl.enabled = true; - } - - function addOidcAccount() { - - addOidcCtrl.enabled = false; - scimFactory.addOpenIDAccount(addOidcCtrl.user.id, addOidcCtrl.issuer, - addOidcCtrl.subject).then(function(response) { - $uibModalInstance.close(response.data); - addOidcCtrl.enabled = true; - }, function(error) { - console.error('Error creating group: ' + error); - $scope.operationResult = Utils.buildErrorOperationResult(error); - addOidcCtrl.enabled = true; - }); - } - - function cancel() { - $uibModalInstance.dismiss("cancel"); - } -} \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-user-saml-account.controller.js b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-user-saml-account.controller.js deleted file mode 100644 index b5a32ecf0..000000000 --- a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-user-saml-account.controller.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -angular.module('dashboardApp').controller('AddSamlAccountController', AddSamlAccountController); - -AddSamlAccountController.$inject = ['$scope', '$uibModalInstance', 'scimFactory', 'Utils', '$state', 'user']; - -function AddSamlAccountController($scope, $uibModalInstance, scimFactory, Utils, $state, user) { - - var addSamlAccountCtrl = this; - addSamlAccountCtrl.user = user; - addSamlAccountCtrl.cancel = cancel; - - addSamlAccountCtrl.addSamlAccount = addSamlAccount; - addSamlAccountCtrl.reset = reset; - - addSamlAccountCtrl.reset(); - - function reset() { - - addSamlAccountCtrl.idpId = ""; - addSamlAccountCtrl.userId = ""; - addSamlAccountCtrl.enabled = true; - }; - - function addSamlAccount() { - - addSamlAccountCtrl.enabled = false; - scimFactory.addSamlId(addSamlAccountCtrl.user.id, addSamlAccountCtrl.idpId, addSamlAccountCtrl.userId).then(function(response) { - $uibModalInstance.close(response.data); - addSamlAccountCtrl.enabled = true; - },function(error) { - console.error('Error creating new saml account: ' + error); - $scope.operationResult = Utils.buildErrorOperationResult(error); - addSamlAccountCtrl.enabled = true; - }); - } - - function cancel() { - $uibModalInstance.dismiss("Cancel"); - } -} \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-user-x509-certificate.controller.js b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-user-x509-certificate.controller.js deleted file mode 100644 index 21a9b650b..000000000 --- a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/add-user-x509-certificate.controller.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict'; - -angular.module('dashboardApp').controller('AddX509CertificateController', - AddX509CertificateController); - -AddX509CertificateController.$inject = [ '$scope', '$uibModalInstance', - 'scimFactory', 'Utils', '$state', 'user' ]; - -function AddX509CertificateController($scope, $uibModalInstance, scimFactory, Utils, - $state, user) { - - var addX509CertCtrl = this; - addX509CertCtrl.user = user; - addX509CertCtrl.cancel = cancel; - - addX509CertCtrl.addCertificate = addCertificate; - addX509CertCtrl.reset = reset; - - addX509CertCtrl.reset(); - - function reset() { - - addX509CertCtrl.label = ""; - addX509CertCtrl.value = ""; - addX509CertCtrl.enabled = true; - } - - function checkBase64Encoding() { - - var base64Matcher = new RegExp( - "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$"); - return (base64Matcher.test(addX509CertCtrl.value)); - } - - function addCertificate() { - - addX509CertCtrl.enabled = false; - - console.log("Adding Certificate ", addX509CertCtrl.label, - addX509CertCtrl.value); - - if (!checkBase64Encoding()) { - $scope.operationResult = Utils.buildErrorOperationResult({ - data: { - detail: "Key is not a base64 encoded string!" - } - }); - return; - } - - scimFactory.addX509Certificate(addX509CertCtrl.user.id, - addX509CertCtrl.label, false, addX509CertCtrl.value).then( - function(response) { - console.log("Added x509 Certificate: ", - addX509CertCtrl.label, addX509CertCtrl.value); - $uibModalInstance.close(response.data); - addX509CertCtrl.enabled = true; - }, function(error) { - console.error('Error creating x509 certificate: ', error); - $scope.operationResult = Utils.buildErrorOperationResult(error); - addX509CertCtrl.enabled = true; - }); - } - - function cancel() { - $uibModalInstance.dismiss("cancel"); - } -} \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/group.controller.js b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/group.controller.js index 8757e45b7..afa9f890e 100644 --- a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/group.controller.js +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/group.controller.js @@ -2,16 +2,22 @@ angular.module('dashboardApp').controller('GroupController', GroupController); -GroupController.$inject = [ '$rootScope', '$state', '$filter', 'scimFactory', '$uibModal', 'ModalService', 'Utils' ]; +GroupController.$inject = [ '$scope', '$rootScope', '$state', '$filter', 'scimFactory', '$uibModal', 'ModalService', 'Utils', 'toaster' ]; -function GroupController($rootScope, $state, $filter, scimFactory, $uibModal, ModalService, Utils) { +function GroupController($scope, $rootScope, $state, $filter, scimFactory, $uibModal, ModalService, Utils, toaster) { var group = this; group.loadGroup = loadGroup; + group.openAddSubgroupDialog = openAddSubgroupDialog; group.id = $state.params.id; group.data = []; + group.subgroups = []; + + group.removeMemberFromList = removeMemberFromList; + group.deleteMember = deleteMember; + group.deleteGroup = deleteGroup; group.loadGroup(); @@ -32,6 +38,7 @@ function GroupController($rootScope, $state, $filter, scimFactory, $uibModal, Mo group.data.members = $filter('orderBy')(group.data.members, "display", false); $rootScope.pageLoadingProgress = 100; group.loadingModal.dismiss("Dismiss"); + separateSubGroups(); }, function(error) { @@ -43,36 +50,111 @@ function GroupController($rootScope, $state, $filter, scimFactory, $uibModal, Mo }); } - group.removeMemberFromList = removeMemberFromList; - - function removeMemberFromList(user) { + function removeMemberFromList(member) { - var i = group.data.members.indexOf(user); + var i = group.data.members.indexOf(member); group.data.members.splice(i, 1); } + + function removeSubgroupFromList(member) { - group.deleteMember = deleteMember; + var i = group.subgroups.indexOf(member); + group.subgroups.splice(i, 1); + } - function deleteMember(user) { + function deleteMember(member) { var modalOptions = { closeButtonText: 'Cancel', actionButtonText: 'Remove membership', - headerText: 'Remove «' + user.display + "» from «" + group.data.displayName + "»", - bodyText: `Are you sure you want to remove '${user.display}' from '${group.data.displayName}'?` + headerText: 'Remove «' + member.display + "» from «" + group.data.displayName + "»", + bodyText: `Are you sure you want to remove '${member.display}' from '${group.data.displayName}'?` }; ModalService.showModal({}, modalOptions).then( function (){ - scimFactory.removeUserFromGroup(group.id, user.value, user.$ref, - user.display) + scimFactory.removeMemberFromGroup(group.id, member.value, member.$ref, + member.display) .then(function(response) { - console.log("Deleted: ", user.display); - group.removeMemberFromList(user); - $scope.operationResult = Utils.buildSuccessOperationResult("User " + user.display + " membership has been removed successfully"); + console.log("Deleted: ", member.display); + group.removeMemberFromList(member); + toaster.pop({ + type: 'success', + body: + `Member ${member.display} has been removed successfully` + }); }, function(error) { - $scope.operationResult = Utils.buildErrorOperationResult(error); + toaster.pop({ + type: 'error', + body: + `${error.data.detail}` + }); }); }); } -} \ No newline at end of file + + function separateSubGroups(){ + if(group.data.members){ + group.subgroups = group.data.members.filter(memberIsAGroup); + group.subgroups.forEach(function(member){ + removeMemberFromList(member); + }); + }else{ + group.subgroups = []; + } + } + + function memberIsAGroup(member){ + return (member.$ref.indexOf('scim/Groups')!=-1); + } + + function openAddSubgroupDialog() { + var modalInstance = $uibModal.open({ + templateUrl: '/resources/iam/template/dashboard/group/addsubgroup.html', + controller: 'AddSubGroupController', + controllerAs: 'addSubGroupCtrl', + resolve: {group: function() { return group.data; }} + }); + modalInstance.result.then(function(createdGroup) { + console.info(createdGroup); + group.loadGroup(); + toaster.pop({ + type: 'success', + body: + `Group '${createdGroup.displayName}' CREATED successfully` + }); + }, function() { + console.info('Modal dismissed at: ', new Date()); + }); + } + + function deleteGroup(member) { + + var modalOptions = { + closeButtonText: 'Cancel', + actionButtonText: 'Delete Group', + headerText: "Delete Group «" + member.display + "»", + bodyText: `Are you sure you want to delete group '${member.display}'?` + }; + + ModalService.showModal({}, modalOptions).then( + function (){ + scimFactory.deleteGroup(member.value) + .then(function(response) { + removeSubgroupFromList(member); + $rootScope.loggedUser.totGroups = $rootScope.loggedUser.totGroups -1; + toaster.pop({ + type: 'success', + body: + `Group '${member.display}' DELETED successfully` + }); + }, function(error) { + toaster.pop({ + type: 'error', + body: + `${error.data.detail}` + }); + }); + }); + } +} diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/groups.controller.js b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/groups.controller.js index f1faeccea..78ca30980 100644 --- a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/groups.controller.js +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/controllers/groups.controller.js @@ -3,10 +3,10 @@ angular.module('dashboardApp').controller('GroupsController', GroupsController); GroupsController.$inject = [ '$scope', '$rootScope', '$uibModal', '$state', - '$filter', 'filterFilter', 'scimFactory', 'ModalService', 'Utils' ]; + '$filter', 'filterFilter', 'scimFactory', 'ModalService', 'Utils', 'toaster' ]; function GroupsController($scope, $rootScope, $uibModal, $state, $filter, - filterFilter, scimFactory, ModalService, Utils) { + filterFilter, scimFactory, ModalService, Utils, toaster) { var gc = this; @@ -29,7 +29,7 @@ function GroupsController($scope, $rootScope, $uibModal, $state, $filter, gc.rebuildFilteredList = rebuildFilteredList; // add group - gc.clickToOpen = clickToOpen; + gc.openAddGroupDialog = openAddGroupDialog; // delete group gc.deleteGroup = deleteGroup; @@ -38,10 +38,9 @@ function GroupsController($scope, $rootScope, $uibModal, $state, $filter, gc.loadGroupList = loadGroupList; // Controller actions: - gc.resetFilters() + gc.resetFilters(); gc.loadGroupList(); // eval gc.groups - function rebuildFilteredList() { gc.filtered = filterFilter(gc.groups, {'displayName': gc.searchText}); @@ -94,7 +93,9 @@ function GroupsController($scope, $rootScope, $uibModal, $state, $filter, } else { $rootScope.pageLoadingProgress = 100; gc.loadingModal.dismiss("Cancel"); + $rootScope.groups = gc.groups; } + }, function(error) { console.log("getGroups error", error); gc.loadingModal.dismiss("Error"); @@ -102,7 +103,7 @@ function GroupsController($scope, $rootScope, $uibModal, $state, $filter, }); } - function clickToOpen() { + function openAddGroupDialog() { var modalInstance = $uibModal.open({ templateUrl : '/resources/iam/template/dashboard/groups/newgroup.html', controller : 'AddGroupController', @@ -111,13 +112,16 @@ function GroupsController($scope, $rootScope, $uibModal, $state, $filter, modalInstance.result.then(function(createdGroup) { console.info(createdGroup); gc.groups.push(createdGroup); - gc.rebuildFilteredList(); - $scope.operationResult = Utils.buildSuccessOperationResult("Group " + createdGroup.displayName + " CREATED successfully"); + gc.loadGroupList() + toaster.pop({ + type: 'success', + body: + `Group '${createdGroup.displayName}' CREATED successfully` + }); }, function() { console.info('Modal dismissed at: ', new Date()); }); } - ; function removeGroupFromList(group) { @@ -142,9 +146,17 @@ function GroupsController($scope, $rootScope, $uibModal, $state, $filter, .then(function(response) { gc.removeGroupFromList(group); $rootScope.loggedUser.totGroups = $rootScope.loggedUser.totGroups -1; - $scope.operationResult = Utils.buildSuccessOperationResult("Group " + group.displayName + " DELETED successfully"); + toaster.pop({ + type: 'success', + body: + `Group '${group.displayName}' DELETED successfully` + }); }, function(error) { - $scope.operationResult = Utils.buildErrorOperationResult(error); + toaster.pop({ + type: 'error', + body: + `${error.data.detail}` + }); }); }); } diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/dashboard-app.module.js b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/dashboard-app.module.js index f05555662..cba26760f 100644 --- a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/dashboard-app.module.js +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/dashboard-app.module.js @@ -15,6 +15,7 @@ angular.module('dashboardApp') $rootScope.iamVersion = getIamVersion(); $rootScope.iamCommitId = getIamGitCommitId(); + $rootScope.accountLinkingEnabled = getAccountLinkingEnabled(); LoadTemplatesService.loadTemplates(); @@ -76,4 +77,4 @@ angular.module('dashboardApp') $('#body').show(); - }); \ No newline at end of file + }); diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/services/account-linking.service.js b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/services/account-linking.service.js index f9cdd1928..954229895 100644 --- a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/services/account-linking.service.js +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/services/account-linking.service.js @@ -8,22 +8,29 @@ function AccountLinkingService($http) { var OIDC_RESOURCE = '/iam/account-linking/OIDC'; var SAML_RESOURCE = '/iam/account-linking/SAML'; + var X509_RESOURCE = '/iam/account-linking/X509'; var service = { unlinkOidcAccount: unlinkOidcAccount, - unlinkSamlAccount: unlinkSamlAccount + unlinkSamlAccount: unlinkSamlAccount, + unlinkX509Certificate: unlinkX509Certificate }; return service; function unlinkOidcAccount(account) { return $http.delete( - OIDC_RESOURCE, {params: {iss: account.iss, sub: account.sub}}); + OIDC_RESOURCE, {params: account}); } function unlinkSamlAccount(account) { return $http.delete( - SAML_RESOURCE, {params: {iss: account.iss, sub: account.sub}}); + SAML_RESOURCE, {params: account}); + } + + function unlinkX509Certificate(cert) { + return $http.delete( + X509_RESOURCE, {params: {certificateSubject: cert.subjectDn}}); } } diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/services/load-templates.service.js b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/services/load-templates.service.js index 5b6441234..1791037be 100644 --- a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/services/load-templates.service.js +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/services/load-templates.service.js @@ -21,11 +21,8 @@ '/resources/iam/template/dashboard/nav.html', '/resources/iam/template/dashboard/operation-result.html', '/resources/iam/template/dashboard/requests/management.html', - '/resources/iam/template/dashboard/user/addoidc.html', - '/resources/iam/template/dashboard/user/addsamlaccount.html', '/resources/iam/template/dashboard/user/addsshkey.html', '/resources/iam/template/dashboard/user/addusergroup.html', - '/resources/iam/template/dashboard/user/addx509certificate.html', '/resources/iam/template/dashboard/user/assign-vo-admin-privileges.html', '/resources/iam/template/dashboard/user/edituser.html', '/resources/iam/template/dashboard/user/revoke-vo-admin-privileges.html', diff --git a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/services/scim-factory.service.js b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/services/scim-factory.service.js index 7cc9a9bd0..b0f4ebfaa 100644 --- a/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/services/scim-factory.service.js +++ b/iam-login-service/src/main/webapp/resources/iam/js/dashboard-app/services/scim-factory.service.js @@ -1,12 +1,12 @@ -angular.module('dashboardApp').factory("scimFactory", [ '$http', '$httpParamSerializer', function($http, $httpParamSerializer) { +angular.module('dashboardApp').factory("scimFactory", ['$http', '$httpParamSerializer', function ($http, $httpParamSerializer) { var urlBase = '/scim'; var urlUsers = urlBase + '/Users'; var urlGroups = urlBase + '/Groups'; var urlMe = urlBase + '/Me'; - + var service = { - + getUsers: getUsers, getGroups: getGroups, getUser: getUser, @@ -18,6 +18,7 @@ angular.module('dashboardApp').factory("scimFactory", [ '$http', '$httpParamSeri deleteUser: deleteUser, addUserToGroup: addUserToGroup, removeUserFromGroup: removeUserFromGroup, + removeMemberFromGroup: removeMemberFromGroup, addOpenIDAccount: addOpenIDAccount, removeOpenIDAccount: removeOpenIDAccount, addSshKey: addSshKey, @@ -30,51 +31,51 @@ angular.module('dashboardApp').factory("scimFactory", [ '$http', '$httpParamSeri updateUser: updateUser, updateMe: updateMe } - + return service; function getUsers(startIndex, count) { - + console.info("Getting users from-to: ", startIndex, count); var qs = $httpParamSerializer({ 'startIndex': startIndex, 'count': count }); var url = urlUsers + '?' + qs; - + return $http.get(url); }; function getGroups(startIndex, count) { - + console.info("Getting groups from-to: ", startIndex, count); var qs = $httpParamSerializer({ 'startIndex': startIndex, 'count': count }); var url = urlGroups + '?' + qs; - + return $http.get(url); }; function getUser(userId) { - + console.info("Getting user: ", userId); var url = urlUsers + '/' + userId; - + return $http.get(url); }; function getGroup(groupId) { - + console.info("Getting group: ", groupId); var url = urlGroups + '/' + groupId; - + return $http.get(url); }; function getMe() { - + console.info("Getting Me endpoint"); var url = urlBase + '/Me'; @@ -85,74 +86,102 @@ angular.module('dashboardApp').factory("scimFactory", [ '$http', '$httpParamSeri console.info("Creating group: ", group); var config = { - headers : { - 'Accept' : 'application/scim+json', - 'Content-Type' : 'application/scim+json' + headers: { + 'Accept': 'application/scim+json', + 'Content-Type': 'application/scim+json' } } return $http.post(urlGroups, group, config); }; - + function createUser(user) { console.info("Creating user: ", user); var config = { - headers : { - 'Accept' : 'application/scim+json', - 'Content-Type' : 'application/scim+json' + headers: { + 'Accept': 'application/scim+json', + 'Content-Type': 'application/scim+json' } } return $http.post(urlUsers, user, config); }; - + function deleteGroup(groupId) { - + console.info("Deleting group: ", groupId); var url = urlGroups + '/' + groupId; return $http.delete(url); }; - + function deleteUser(userId) { - + console.info("Deleting user: ", userId); var url = urlUsers + '/' + userId; return $http.delete(url); }; - + function addUserToGroup(groupId, scimUser) { - + console.info("Patch groupId, add user ", groupId, scimUser); - + var config = { - headers: { 'Content-Type': 'application/scim+json' } - }; + headers: { + 'Content-Type': 'application/scim+json' + } + }; var data = { - - schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], - operations: [{ - op: "add", - path: "members", - value: [{ - value: scimUser.id, - $ref: scimUser.meta.location, - display: scimUser.displayName - }] + + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + operations: [{ + op: "add", + path: "members", + value: [{ + value: scimUser.id, + $ref: scimUser.meta.location, + display: scimUser.displayName }] + }] }; - + var url = urlGroups + '/' + groupId; return $http.patch(url, data, config); }; function removeUserFromGroup(groupId, userId, userLocation, userDisplayName) { - + console.info("Patch groupId, remove user", groupId, userId, userLocation); + + var config = { + headers: { + 'Content-Type': 'application/scim+json' + } + }; + var data = { + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + operations: [{ + op: "remove", + path: "members", + value: [{ + display: userDisplayName, + value: userId, + $ref: userLocation + }] + }] + }; + var url = urlGroups + '/' + groupId; + + return $http.patch(url, data, config); + }; + + function removeMemberFromGroup(groupId, memberId, memberLocation, memberDisplayName) { + + console.info("Patch groupId, remove member", groupId, memberId, memberLocation); var config = { headers: { 'Content-Type': 'application/scim+json' } @@ -163,9 +192,9 @@ angular.module('dashboardApp').factory("scimFactory", [ '$http', '$httpParamSeri op: "remove", path: "members", value: [{ - display: userDisplayName, - value: userId, - $ref: userLocation + display: memberDisplayName, + value: memberId, + $ref: memberLocation }] }] }; @@ -173,237 +202,262 @@ angular.module('dashboardApp').factory("scimFactory", [ '$http', '$httpParamSeri return $http.patch(url, data, config); }; - - function addOpenIDAccount(userId, issuer, subject) { + + function removeMemberFromGroup(groupId, memberId, memberLocation, memberDisplayName) { - console.info("Patch user-id, add oidc account ", userId, issuer, subject); + console.info("Patch groupId, remove member", groupId, memberId, memberLocation); var config = { headers: { 'Content-Type': 'application/scim+json' } }; var data = { - schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], operations: [{ - op: "add", - value: { - "urn:indigo-dc:scim:schemas:IndigoUser": { - oidcIds: [{ - "issuer": issuer, - "subject": subject - } - ] - } - } + op: "remove", + path: "members", + value: [{ + display: memberDisplayName, + value: memberId, + $ref: memberLocation + }] }] }; - var url = urlUsers + '/' + userId; + var url = urlGroups + '/' + groupId; return $http.patch(url, data, config); }; - function removeOpenIDAccount(userId, issuer, subject) { - - console.info("Patch user-id, remove oidc account ", userId, issuer, subject); + function addOpenIDAccount(userId, account) { + + var url = urlUsers + '/' + userId; var config = { - headers: { 'Content-Type': 'application/scim+json' } - }; + headers: { + 'Content-Type': 'application/scim+json' + } + }; + var data = { - - schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], - operations: [{ - op: "remove", - value: { - "urn:indigo-dc:scim:schemas:IndigoUser": { - oidcIds: [{ - "issuer": issuer, - "subject": subject - } - ] - } + + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + operations: [{ + op: "add", + value: { + "urn:indigo-dc:scim:schemas:IndigoUser": { + oidcIds: [account] } - }] + } + }] }; + + + return $http.patch(url, data, config); + } + + function removeOpenIDAccount(userId, account) { + var url = urlUsers + '/' + userId; + + var config = { + headers: { + 'Content-Type': 'application/scim+json' + } + }; + var data = { + + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + operations: [{ + op: "remove", + value: { + "urn:indigo-dc:scim:schemas:IndigoUser": { + oidcIds: [account] + } + } + }] + }; + return $http.patch(url, data, config); - }; + } function addSshKey(userId, label, isPrimary, value) { - + console.info("Patch user-id, add ssh-key ", userId, label, value); - + var config = { - headers: { 'Content-Type': 'application/scim+json' } - }; + headers: { + 'Content-Type': 'application/scim+json' + } + }; var data = { - schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], - operations: [{ - op: "add", - value: { - "urn:indigo-dc:scim:schemas:IndigoUser": { - sshKeys: [{ - "display": label, - "primary": isPrimary, - "value": value - } - ] - } + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + operations: [{ + op: "add", + value: { + "urn:indigo-dc:scim:schemas:IndigoUser": { + sshKeys: [{ + "display": label, + "primary": isPrimary, + "value": value + }] } - }] + } + }] }; var url = urlUsers + '/' + userId; - + return $http.patch(url, data, config); }; function removeSshKey(userId, fingerprint) { - + console.info("Patch user-id, remove ssh-key ", userId, fingerprint); - + var config = { - headers: { 'Content-Type': 'application/scim+json' } - }; + headers: { + 'Content-Type': 'application/scim+json' + } + }; var data = { - schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], - operations: [{ - op: "remove", - value: { - "urn:indigo-dc:scim:schemas:IndigoUser": { - sshKeys: [{ - "fingerprint": fingerprint - } - ] - } + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + operations: [{ + op: "remove", + value: { + "urn:indigo-dc:scim:schemas:IndigoUser": { + sshKeys: [{ + "fingerprint": fingerprint + }] } - }] + } + }] }; var url = urlUsers + '/' + userId; - + return $http.patch(url, data, config); }; - - function addX509Certificate(userId, label, isPrimary, value) { - - console.info("Patch user-id, add ssh-key ", userId, label, value); - + + function addX509Certificate(userId, certificate) { + + var url = urlUsers + '/' + userId; + var config = { - headers: { 'Content-Type': 'application/scim+json' } - }; + headers: { + 'Content-Type': 'application/scim+json' + } + }; + var data = { - schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], - operations: [{ - op: "add", - value: { - x509Certificates: [{ - "display": label, - "primary": isPrimary, - "value": value - }] - } - }] - }; - var url = urlUsers + '/' + userId; - + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + operations: [{ + op: "add", + value: { + "urn:indigo-dc:scim:schemas:IndigoUser": { + certificates: [certificate] + } + } + }] + }; + return $http.patch(url, data, config); }; - - function removeX509Certificate(userId, value) { - - console.info("Patch user-id, add ssh-key ", userId, value); - + + function removeX509Certificate(userId, certificate) { + var config = { - headers: { 'Content-Type': 'application/scim+json' } - }; + headers: { + 'Content-Type': 'application/scim+json' + } + }; + var data = { - schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], - operations: [{ - op: "remove", - value: { - x509Certificates: [{ - "value": value - }] - } - }] - }; + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + operations: [{ + op: "remove", + value: { + "urn:indigo-dc:scim:schemas:IndigoUser": { + certificates: [certificate] + } + } + }] + }; var url = urlUsers + '/' + userId; - + return $http.patch(url, data, config); }; - - function addSamlId(userId, samlIdpId, samlUserId) { - - console.info("Patch user-id, add saml-account ", userId, samlIdpId, samlUserId); + + function addSamlId(userId, samlId) { + + var url = urlUsers + '/' + userId; var config = { - headers: { 'Content-Type': 'application/scim+json' } - }; + headers: { + 'Content-Type': 'application/scim+json' + } + }; var data = { - schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], - operations: [{ - op: "add", - value: { - "urn:indigo-dc:scim:schemas:IndigoUser": { - samlIds: [{ - "idpId": samlIdpId, - "userId": samlUserId - } - ] - } + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + operations: [{ + op: "add", + value: { + "urn:indigo-dc:scim:schemas:IndigoUser": { + samlIds: [samlId] } - }] - }; - var url = urlUsers + '/' + userId; + } + }] + }; return $http.patch(url, data, config); - }; - + } + function removeSamlId(userId, samlId) { - + console.info("Patch user-id, remove saml-account ", userId, samlId.idpId, samlId.userId); - + var config = { - headers: { 'Content-Type': 'application/scim+json' } - }; + headers: { + 'Content-Type': 'application/scim+json' + } + }; var data = { - schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], - operations: [{ - op: "remove", - value: { - "urn:indigo-dc:scim:schemas:IndigoUser": { - samlIds: [{ - "idpId": samlId.idpId, - "userId": samlId.userId - } - ] - } + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + operations: [{ + op: "remove", + value: { + "urn:indigo-dc:scim:schemas:IndigoUser": { + samlIds: [{ + "idpId": samlId.idpId, + "attributeId": samlId.attributeId, + "userId": samlId.userId + }] } - }] - }; + } + }] + }; var url = urlUsers + '/' + userId; - + return $http.patch(url, data, config); }; - + function setUserActiveStatus(userId, status) { - + console.info("Patch user-id, set active to ", userId, status); - + var config = { - headers: { 'Content-Type': 'application/scim+json' } - }; + headers: { + 'Content-Type': 'application/scim+json' + } + }; var data = { - schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], - operations: [{ - op: "replace", - value: { - active: status - } - }] - }; + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + operations: [{ + op: "replace", + value: { + active: status + } + }] + }; var url = urlUsers + '/' + userId; - + return $http.patch(url, data, config); }; @@ -412,12 +466,14 @@ angular.module('dashboardApp').factory("scimFactory", [ '$http', '$httpParamSeri console.info("Patch user ", userId, ops); var config = { - headers: { 'Content-Type': 'application/scim+json' } - }; + headers: { + 'Content-Type': 'application/scim+json' + } + }; var data = { - schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], - operations: ops - }; + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + operations: ops + }; var url = urlUsers + '/' + userId; return $http.patch(url, data, config); @@ -428,15 +484,17 @@ angular.module('dashboardApp').factory("scimFactory", [ '$http', '$httpParamSeri console.info("Patch current user", ops); var config = { - headers: { 'Content-Type': 'application/scim+json' } - }; + headers: { + 'Content-Type': 'application/scim+json' + } + }; var data = { - schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], - operations: ops - }; + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + operations: ops + }; return $http.patch(urlMe, data, config); }; return scimFactory; -} ]); +}]); \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/template/dashboard/group/addsubgroup.html b/iam-login-service/src/main/webapp/resources/iam/template/dashboard/group/addsubgroup.html new file mode 100644 index 000000000..aa1b6c52d --- /dev/null +++ b/iam-login-service/src/main/webapp/resources/iam/template/dashboard/group/addsubgroup.html @@ -0,0 +1,30 @@ + + + diff --git a/iam-login-service/src/main/webapp/resources/iam/template/dashboard/group/group.html b/iam-login-service/src/main/webapp/resources/iam/template/dashboard/group/group.html index db83d306b..039808a6b 100644 --- a/iam-login-service/src/main/webapp/resources/iam/template/dashboard/group/group.html +++ b/iam-login-service/src/main/webapp/resources/iam/template/dashboard/group/group.html @@ -24,6 +24,46 @@

+
+
+

Subgroups

+ +
+
+

No subgroup found.

+ + + + + + + + + + + + + + + + + + + + + + +
#NameActions
{{$index + 1}}{{member.display}} + +
#NameActions
+
+ +
+

Members

@@ -41,11 +81,11 @@

Members

- - {{$index + 1}} - {{user.display}} - - @@ -59,11 +99,9 @@

Members

-
- - \ No newline at end of file + diff --git a/iam-login-service/src/main/webapp/resources/iam/template/dashboard/groups/groups.html b/iam-login-service/src/main/webapp/resources/iam/template/dashboard/groups/groups.html index c8e9a9db2..3909d0dfc 100644 --- a/iam-login-service/src/main/webapp/resources/iam/template/dashboard/groups/groups.html +++ b/iam-login-service/src/main/webapp/resources/iam/template/dashboard/groups/groups.html @@ -47,10 +47,10 @@

- {{$index + (gc.currentPage-1)*gc.entryLimit + 1}} - {{group.displayName}} + {{$index + (gc.currentPage-1)*gc.entryLimit + 1}} + {{group.displayName}} - + @@ -72,10 +72,9 @@

- \ No newline at end of file + diff --git a/iam-login-service/src/main/webapp/resources/iam/template/dashboard/groups/newgroup.html b/iam-login-service/src/main/webapp/resources/iam/template/dashboard/groups/newgroup.html index c76334491..dfbfbb5ec 100644 --- a/iam-login-service/src/main/webapp/resources/iam/template/dashboard/groups/newgroup.html +++ b/iam-login-service/src/main/webapp/resources/iam/template/dashboard/groups/newgroup.html @@ -2,18 +2,34 @@

diff --git a/iam-login-service/src/main/webapp/resources/iam/template/dashboard/user/addoidc.html b/iam-login-service/src/main/webapp/resources/iam/template/dashboard/user/addoidc.html deleted file mode 100644 index 4d6270fb9..000000000 --- a/iam-login-service/src/main/webapp/resources/iam/template/dashboard/user/addoidc.html +++ /dev/null @@ -1,28 +0,0 @@ - - - \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/template/dashboard/user/addsamlaccount.html b/iam-login-service/src/main/webapp/resources/iam/template/dashboard/user/addsamlaccount.html deleted file mode 100644 index 8324f98fe..000000000 --- a/iam-login-service/src/main/webapp/resources/iam/template/dashboard/user/addsamlaccount.html +++ /dev/null @@ -1,28 +0,0 @@ - - - \ No newline at end of file diff --git a/iam-login-service/src/main/webapp/resources/iam/template/dashboard/user/addusergroup.html b/iam-login-service/src/main/webapp/resources/iam/template/dashboard/user/addusergroup.html index 8a9654730..0c517e28b 100644 --- a/iam-login-service/src/main/webapp/resources/iam/template/dashboard/user/addusergroup.html +++ b/iam-login-service/src/main/webapp/resources/iam/template/dashboard/user/addusergroup.html @@ -2,21 +2,20 @@ - - {{addGroupCtrl.loadingGroupsProgress}} / 100 + + {{addGroupCtrl.loadingGroupsProgress}} / 100