diff --git a/apps/meteor/app/meteor-accounts-saml/server/startup.ts b/apps/meteor/app/meteor-accounts-saml/server/startup.ts index 7a2bf16d3244e..556ab7df13e7d 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/startup.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/startup.ts @@ -1,4 +1,5 @@ import { Logger } from '@rocket.chat/logger'; +import debounce from 'lodash.debounce'; import { Meteor } from 'meteor/meteor'; import { settings } from '../../settings/server'; @@ -10,5 +11,6 @@ SAMLUtils.setLoggerInstance(logger); Meteor.startup(async () => { await addSettings('Default'); - settings.watchByRegex(/^SAML_.+/, loadSamlServiceProviders); }); + +settings.watchByRegex(/^SAML_.+/, debounce(loadSamlServiceProviders, 2000)); diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 4a7ee0f11474d..8fcc9eeac067b 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -168,6 +168,7 @@ "chai-dom": "^1.11.0", "chai-spies": "~1.0.0", "cross-env": "^7.0.3", + "docker-compose": "^0.24.3", "emojione-assets": "^4.5.0", "eslint": "~8.45.0", "eslint-config-prettier": "~8.8.0", diff --git a/apps/meteor/tests/e2e/config/constants.ts b/apps/meteor/tests/e2e/config/constants.ts index 3e9b693cc7dcd..c938b693ff450 100644 --- a/apps/meteor/tests/e2e/config/constants.ts +++ b/apps/meteor/tests/e2e/config/constants.ts @@ -15,3 +15,8 @@ export const ADMIN_CREDENTIALS = { password: 'rocketchat.internal.admin.test', username: 'rocketchat.internal.admin.test', } as const; + +export const DEFAULT_USER_CREDENTIALS = { + password: 'password', + bcrypt: '$2b$10$LNYaqDreDE7tt9EVEeaS9uw.C3hic9hcqFfIocMBPTMxJaDCC6QWW', +} as const; diff --git a/apps/meteor/tests/e2e/containers/saml/Dockerfile b/apps/meteor/tests/e2e/containers/saml/Dockerfile new file mode 100644 index 0000000000000..be87fdb742b65 --- /dev/null +++ b/apps/meteor/tests/e2e/containers/saml/Dockerfile @@ -0,0 +1,35 @@ +FROM php:7.1-apache + +# Utilities +RUN apt-get update && \ + apt-get -y install apt-transport-https git curl vim --no-install-recommends && \ + rm -r /var/lib/apt/lists/* + +# SimpleSAMLphp +ARG SIMPLESAMLPHP_VERSION=1.15.2 +RUN curl -s -L -o /tmp/simplesamlphp.tar.gz https://github.com/simplesamlphp/simplesamlphp/releases/download/v$SIMPLESAMLPHP_VERSION/simplesamlphp-$SIMPLESAMLPHP_VERSION.tar.gz && \ + tar xzf /tmp/simplesamlphp.tar.gz -C /tmp && \ + rm -f /tmp/simplesamlphp.tar.gz && \ + mv /tmp/simplesamlphp-* /var/www/simplesamlphp && \ + touch /var/www/simplesamlphp/modules/exampleauth/enable +COPY config/simplesamlphp/config.php /var/www/simplesamlphp/config +COPY config/simplesamlphp/authsources.php /var/www/simplesamlphp/config +COPY config/simplesamlphp/saml20-sp-remote.php /var/www/simplesamlphp/metadata +COPY config/simplesamlphp/server_crt /var/www/simplesamlphp/cert/server.crt +COPY config/simplesamlphp/server_pem /var/www/simplesamlphp/cert/server.pem + +# Apache +COPY config/apache/ports.conf /etc/apache2 +COPY config/apache/simplesamlphp.conf /etc/apache2/sites-available +COPY config/apache/cert_crt /etc/ssl/cert/cert.crt +COPY config/apache/private_key /etc/ssl/private/private.key +RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf && \ + a2enmod ssl && \ + a2dissite 000-default.conf default-ssl.conf && \ + a2ensite simplesamlphp.conf + +# Set work dir +WORKDIR /var/www/simplesamlphp + +# General setup +EXPOSE 8080 8443 \ No newline at end of file diff --git a/apps/meteor/tests/e2e/containers/saml/config/apache/cert_crt b/apps/meteor/tests/e2e/containers/saml/config/apache/cert_crt new file mode 100644 index 0000000000000..4104eaf80f6e4 --- /dev/null +++ b/apps/meteor/tests/e2e/containers/saml/config/apache/cert_crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJANdMEUvsTntJMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzMjIxWhcNNDgwNjI1MTQzMjIxWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAyXRKD3KzV/hOqwThFRtA+eoitJpIIEmWbugPMC1G+7bFqxHmtxiuQhOw +yHrij35biiD8VpboY69Zep7n1QCfmIodh9uxdaNxFtzxjryRLzfP3MpPFkpBHCdV +HZWDP2TzIvOxWcnlLmikSnBrBM1nvhKSWjaFsjDAXMLXT0mceiDpQ0QQkDA6RAyx +JWWRJILjudBh56ukqvdz4eFWAAViZX5MUwCDxiBxtP3NIXVmODM7kDLqZ9+QcfpM +N5QvfcUjHP9584yrYiJ9N64Fy5vU2OH1RX5EsMHTtdqR4H5K6zNfVlgRSG170Mcj +ksTSbo1kcDCSuzTO82NrVkU+R78W/QIDAQABo1AwTjAdBgNVHQ4EFgQUkYMQMPqY +leTTtBHS1f7yNFmY86QwHwYDVR0jBBgwFoAUkYMQMPqYleTTtBHS1f7yNFmY86Qw +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAGZpbEWzYLoa5keg9rQDa +S2cf9rQFMNflwR7hQ6OtSXeP0JsQ6yFhRq3TgpBdbkmdealDJbyG8pWQxqwOBD/j +45jr+NsHap0HvQAg9pfq/QIqjH5osCGAHmNdHfP638FOpi0s6hAX11+nCW6ClHMO +ofn0fYXCBtM18qZvtoeB8swi6MlRXFLvvRL4tWzGugdQj+0f+ukk/GZAEitdpkuj +qFCHqfKwg9FJ4My5M8lIyB/P4+SK/ail/BoCJ/qGBXky2bob0MQuLysd35zrTA62 +j5IvPp1XZj/2KnuPtqMuFhNtE5wCnOEC1WG02ZVIfs4DAxX78z59VSEaFlnstT9k +aQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/apps/meteor/tests/e2e/containers/saml/config/apache/ports.conf b/apps/meteor/tests/e2e/containers/saml/config/apache/ports.conf new file mode 100644 index 0000000000000..286af7fd4ec36 --- /dev/null +++ b/apps/meteor/tests/e2e/containers/saml/config/apache/ports.conf @@ -0,0 +1,9 @@ +Listen 8080 + + + Listen 8443 + + + + Listen 8443 + \ No newline at end of file diff --git a/apps/meteor/tests/e2e/containers/saml/config/apache/private_key b/apps/meteor/tests/e2e/containers/saml/config/apache/private_key new file mode 100644 index 0000000000000..f511efcd9b655 --- /dev/null +++ b/apps/meteor/tests/e2e/containers/saml/config/apache/private_key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJdEoPcrNX+E6r +BOEVG0D56iK0mkggSZZu6A8wLUb7tsWrEea3GK5CE7DIeuKPfluKIPxWluhjr1l6 +nufVAJ+Yih2H27F1o3EW3PGOvJEvN8/cyk8WSkEcJ1UdlYM/ZPMi87FZyeUuaKRK +cGsEzWe+EpJaNoWyMMBcwtdPSZx6IOlDRBCQMDpEDLElZZEkguO50GHnq6Sq93Ph +4VYABWJlfkxTAIPGIHG0/c0hdWY4MzuQMupn35Bx+kw3lC99xSMc/3nzjKtiIn03 +rgXLm9TY4fVFfkSwwdO12pHgfkrrM19WWBFIbXvQxyOSxNJujWRwMJK7NM7zY2tW +RT5Hvxb9AgMBAAECggEAVT4KtHyxXJDqIL1gzICKvvUOmGMMD/VzXRx+iMEv3wTY +oWliuakM21LfpAUzZspty4XnoHAch0nET/l7WYr4/R+8HSed8IwnJyh4YhByUouI +PgGw81qaMGKIRotkTOfXZbu+GKMwgbGvivwEnLSZqDjNirS1X8/3JYkgeCFKv/X6 +K4Z1SKoebtA4oGDGZtxNpeGJ6TibaSO3PRW+oH3dD5j5ez1k+dlRasMnuKG76jBy +naz052t52UZxAnGkbZUU84VljO5R8x1jip+M6+qILC/PsI1hb45Dx5VRXNUBGP2s +/PRVFptNGmsIlXBYMSdOo9RJAIsZ1kTty34nIqeuYQKBgQD4Nv+QoeRXjG7iAmzj +JabcA+2FiHftvjdaYjahXQ/Ma6ns/vv//tVfxJaNyCxMObjIKhidsrf/AAn03FgT +LkL0Q2tTBNL9jejzHstnCKSSu/pSgqatsABm1cokC9qsuMjweMVy+tGjSsF6YgjI +2K0heQpF0bMBSYmyQU4aTLbytQKBgQDPxdWVn7ReCscDwpchx+zffKh5JOshW44z +GYBfVbugBljj9w6fDjKyQzk0jMGmuG9rvqfZzu9MpQhjBFKxL4aXPBN7bQOY3DKc +FuGSbIbV7ldPmZhOxqLy6NLyiry6eSQGNf1Q8YCF6bsUR/rvdKDtBLrbJObgRVbu +ChT2PXRYKQKBgFSEDZMGvMReqebE4qSZTm592+Nq60MFUL2y0V0yXc3CHxL2Y4Hw +GGFKg+T08rhlsxhc1RLlJqdqMPmyCT9Gsj+PsTyMWPdC2b3mj2We2MKpxPtRR0W+ +tvRM+U46xxOmu6y9wqV65+TM8IImXU1eEd1i5G+Pjn7ytjL+74Qe+PA9AoGAEasM +F5YmG10lQU+Z1HiQzwxlsy+Ngx+q/vNrNDAxLVF8253Vs3bcnsYSpkJV8Vx7tRjY +YzAyrzzVcr4aXhDhjBjCu1sw1B3de+KCOhZafPSwngc8qW5AyxE7Zv6fP+gvRQvw +R6LRwBF5JCde0mADk0Q0s4/2xhl/Y+ydjbb6HskCgYEAysuIUrDslGGsK57loxMO +FVz9SmLTZIJqkSW+l3dDHMG+BvnJFP+yf0Kr2zGbXRzOvNVWAFP2aU39RFDxbUIM +Nz7obLWrVKbQUDaU7fCbP4OBVuo9p4UM6j/PZy+3Cyps+GMTrC9wi4HbbNEWHyCW +xaNL9LNQ3B9hJG77htK1oRw= +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/apps/meteor/tests/e2e/containers/saml/config/apache/simplesamlphp.conf b/apps/meteor/tests/e2e/containers/saml/config/apache/simplesamlphp.conf new file mode 100644 index 0000000000000..a81c39d85f8b7 --- /dev/null +++ b/apps/meteor/tests/e2e/containers/saml/config/apache/simplesamlphp.conf @@ -0,0 +1,23 @@ + + ServerName localhost + DocumentRoot /var/www/simplesamlphp + Alias /simplesaml /var/www/simplesamlphp/www + + + Require all granted + + + + + ServerName localhost + DocumentRoot /var/www/simplesamlphp + SSLEngine on + SSLCertificateFile /etc/ssl/cert/cert.crt + SSLCertificateKeyFile /etc/ssl/private/private.key + Alias /simplesaml /var/www/simplesamlphp/www + + + Require all granted + + + \ No newline at end of file diff --git a/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/authsources.php b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/authsources.php new file mode 100644 index 0000000000000..867d66049b3ac --- /dev/null +++ b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/authsources.php @@ -0,0 +1,34 @@ + array( + 'core:AdminPassword', + ), + + 'example-userpass' => array( + 'exampleauth:UserPass', + 'samluser1:password' => array( + 'uid' => array('1'), + 'username' => 'samluser1', + 'cn' => 'Saml User 1', + 'eduPersonAffiliation' => array('group1'), + 'email' => 'samluser1@example.com', + ), + 'samluser2:password' => array( + 'uid' => array('2'), + 'username' => 'samluser2', + 'cn' => 'Saml User 2', + 'eduPersonAffiliation' => array('group2'), + 'email' => 'user_for_saml_merge@email.com', + ), + 'samluser3:password' => array( + 'uid' => array('3'), + 'username' => 'user_for_saml_merge2', + 'cn' => 'Saml User 3', + 'eduPersonAffiliation' => array('group2'), + 'email' => 'samluser3@example.com', + ), + ), + +); \ No newline at end of file diff --git a/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/config.php b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/config.php new file mode 100644 index 0000000000000..79e73e6b58c43 --- /dev/null +++ b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/config.php @@ -0,0 +1,111 @@ + 'simplesaml/', + 'certdir' => 'cert/', + 'loggingdir' => 'log/', + 'datadir' => 'data/', + 'tempdir' => '/tmp/simplesaml', + 'debug' => true, + 'showerrors' => true, + 'errorreporting' => true, + 'debug.validatexml' => false, + 'auth.adminpassword' => 'secret', + 'admin.protectindexpage' => false, + 'admin.protectmetadata' => false, + 'secretsalt' => 'defaultsecretsalt', + 'technicalcontact_name' => 'Administrator', + 'technicalcontact_email' => 'na@example.org', + 'timezone' => null, + 'logging.level' => SimpleSAML_Logger::DEBUG, + 'logging.handler' => 'errorlog', + //'logging.format' => '%date{%b %d %H:%M:%S} %process %level %stat[%trackid] %msg', + 'logging.facility' => defined('LOG_LOCAL5') ? constant('LOG_LOCAL5') : LOG_USER, + 'logging.processname' => 'simplesamlphp', + 'logging.logfile' => 'simplesamlphp.log', + 'statistics.out' => array( + ), + 'database.dsn' => 'mysql:host=localhost;dbname=saml', + 'database.username' => 'simplesamlphp', + 'database.password' => 'secret', + 'database.prefix' => '', + 'database.persistent' => false, + 'database.slaves' => array( + ), + 'enable.saml20-idp' => true, + 'enable.shib13-idp' => true, + 'enable.adfs-idp' => false, + 'enable.wsfed-sp' => false, + 'enable.authmemcookie' => false, + 'session.duration' => 8 * (60 * 60), // 8 hours. + 'session.datastore.timeout' => (4 * 60 * 60), // 4 hours + 'session.state.timeout' => (60 * 60), // 1 hour + 'session.cookie.name' => 'SimpleSAMLSessionIDIdp', + 'session.cookie.lifetime' => 0, + 'session.cookie.path' => '/', + 'session.cookie.domain' => null, + 'session.cookie.secure' => false, + 'enable.http_post' => false, + 'session.phpsession.cookiename' => 'PHPSESSIDIDP', + 'session.phpsession.savepath' => null, + 'session.phpsession.httponly' => true, + 'session.authtoken.cookiename' => 'SimpleSAMLAuthTokenIdp', + 'session.rememberme.enable' => false, + 'session.rememberme.checked' => false, + 'session.rememberme.lifetime' => (14 * 86400), + 'language.available' => array( + 'en', 'no', 'nn', 'se', 'da', 'de', 'sv', 'fi', 'es', 'fr', 'it', 'nl', 'lb', 'cs', + 'sl', 'lt', 'hr', 'hu', 'pl', 'pt', 'pt-br', 'tr', 'ja', 'zh', 'zh-tw', 'ru', 'et', + 'he', 'id', 'sr', 'lv', 'ro', 'eu' + ), + 'language.rtl' => array('ar', 'dv', 'fa', 'ur', 'he'), + 'language.default' => 'en', + 'language.parameter.name' => 'language', + 'language.parameter.setcookie' => true, + 'language.cookie.name' => 'language', + 'language.cookie.domain' => null, + 'language.cookie.path' => '/', + 'language.cookie.lifetime' => (60 * 60 * 24 * 900), + 'attributes.extradictionary' => null, + 'theme.use' => 'default', + 'default-wsfed-idp' => 'urn:federation:pingfederate:localhost', + 'idpdisco.enableremember' => true, + 'idpdisco.rememberchecked' => true, + 'idpdisco.validate' => true, + 'idpdisco.extDiscoveryStorage' => null, + 'idpdisco.layout' => 'dropdown', + 'shib13.signresponse' => true, + 'authproc.idp' => array( + 30 => 'core:LanguageAdaptor', + 45 => array( + 'class' => 'core:StatisticsWithAttribute', + 'attributename' => 'realm', + 'type' => 'saml20-idp-SSO', + ), + 50 => 'core:AttributeLimit', + 99 => 'core:LanguageAdaptor', + ), + 'authproc.sp' => array( + 90 => 'core:LanguageAdaptor', + ), + 'metadata.sources' => array( + array('type' => 'flatfile'), + ), + 'store.type' => 'phpsession', + 'store.sql.dsn' => 'sqlite:/path/to/sqlitedatabase.sq3', + 'store.sql.username' => null, + 'store.sql.password' => null, + 'store.sql.prefix' => 'SimpleSAMLphp', + 'memcache_store.servers' => array( + array( + array('hostname' => 'localhost'), + ), + ), + 'memcache_store.prefix' => null, + 'memcache_store.expires' => 36 * (60 * 60), // 36 hours. + 'metadata.sign.enable' => false, + 'metadata.sign.privatekey' => null, + 'metadata.sign.privatekey_pass' => null, + 'metadata.sign.certificate' => null, + 'proxy' => null, + 'trusted.url.domains' => array(), +); \ No newline at end of file diff --git a/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/saml20-sp-remote.php b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/saml20-sp-remote.php new file mode 100644 index 0000000000000..79591af7dfa3d --- /dev/null +++ b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/saml20-sp-remote.php @@ -0,0 +1,24 @@ + 'http://localhost:3000/_saml/metadata/test-sp', + 'contacts' => array ( + ), + 'metadata-set' => 'saml20-sp-remote', + 'AssertionConsumerService' => array ( + 0 => array ( + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + 'Location' => 'http://localhost:3000/_saml/validate/test-sp', + 'index' => 1, + 'isDefault' => true, + ), + ), + 'SingleLogoutService' => array ( + 0 => array ( + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + 'Location' => 'http://localhost:3000/_saml/logout/test-sp/', + 'ResponseLocation' => 'http://localhost:3000/_saml/logout/test-sp/', + ), + ), + 'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', +); \ No newline at end of file diff --git a/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/server_crt b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/server_crt new file mode 100644 index 0000000000000..f0623b9305e07 --- /dev/null +++ b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/server_crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+Cgav +Og8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+ +YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc ++TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyix +YFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8 +jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/C +YQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkw +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6b +lEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFs +X1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7 +yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7 +NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG +99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2n +aQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/server_pem b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/server_pem new file mode 100644 index 0000000000000..ba2bac5fe6308 --- /dev/null +++ b/apps/meteor/tests/e2e/containers/saml/config/simplesamlphp/server_pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDNQIWjOA1vWHUz +SPM1FIKOE4GdH65VtWlpZ9dghH4CFYN0R7mvJj4KBq86Dxt8vJvLMV16GVh0NGCR +50QH8aMbxonDTqXSoXiMM4DDSQTKBYK7aZwftc7FG35gAfdNUdr8e7VbdaPOShuq +qotDyCQpZYzbt86ABnoaJ5okE3pUFIwxw97LcdYsGZz5Ngma/V1to7aMeEqHyl8r +DRbXZUzw/U8g7yC/g+G7+64liJ4FYqLEETLLSUePKLFgUJHXbF2HgIDjur3nxlEa +ecNQYVUTVCGBFpwkI5n1t3m32avwotpUFhMImjkRETyPKZpvl0+p7mop8mwJmKpa +CVuNSj23AgMBAAECggEABn4I/B20xxXcNzASiVZJvua9DdRHtmxTlkLznBj0x2oY +y1/Nbs3d3oFRn5uEuhBZOTcphsgwdRSHDXZsP3gUObew+d2N/zieUIj8hLDVlvJP +rU/s4U/l53Q0LiNByE9ThvL+zJLPCKJtd5uHZjB5fFm69+Q7gu8xg4xHIub+0pP5 +PHanmHCDrbgNN/oqlar4FZ2MXTgekW6Amyc/koE9hIn4Baa2Ke/B/AUGY4pMRLqp +TArt+GTVeWeoFY9QACUpaHpJhGb/Piou6tlU57e42cLoki1f0+SARsBBKyXA7BB1 +1fMH10KQYFA68dTYWlKzQau/K4xaqg4FKmtwF66GQQKBgQD9OpNUS7oRxMHVJaBR +TNWW+V1FXycqojekFpDijPb2X5CWV16oeWgaXp0nOHFdy9EWs3GtGpfZasaRVHsX +SHtPh4Nb8JqHdGE0/CD6t0+4Dns8Bn9cSqtdQB7R3Jn7IMXi9X/U8LDKo+A18/Jq +V8VgUngMny9YjMkQIbK8TRWkYQKBgQDPf4nxO6ju+tOHHORQty3bYDD0+OV3I0+L +0yz0uPreryBVi9nY43KakH52D7UZEwwsBjjGXD+WH8xEsmBWsGNXJu025PvzIJoz +lAEiXvMp/NmYp+tY4rDmO8RhyVocBqWHzh38m0IFOd4ByFD5nLEDrA3pDVo0aNgY +n0GwRysZFwKBgQDkCj3m6ZMUsUWEty+aR0EJhmKyODBDOnY09IVhH2S/FexVFzUN +LtfK9206hp/Awez3Ln2uT4Zzqq5K7fMzUniJdBWdVB004l8voeXpIe9OZuwfcBJ9 +gFi1zypx/uFDv421BzQpBN+QfOdKbvbdQVFjnqCxbSDr80yVlGMrI5fbwQKBgG09 +oRrepO7EIO8GN/GCruLK/ptKGkyhy3Q6xnVEmdb47hX7ncJA5IoZPmrblCVSUNsw +n11XHabksL8OBgg9rt8oQEThQv/aDzTOW9aDlJNragejiBTwq99aYeZ1gjo1CZq4 +2jKubpCfyZC4rGDtrIfZYi1q+S2UcQhtd8DdhwQbAoGAAM4EpDA4yHB5yiek1p/o +CbqRCta/Dx6Eyo0KlNAyPuFPAshupG4NBx7mT2ASfL+2VBHoi6mHSri+BDX5ryYF +fMYvp7URYoq7w7qivRlvvEg5yoYrK13F2+Gj6xJ4jEN9m0KdM/g3mJGq0HBTIQrp +Sm75WXsflOxuTn08LbgGc4s= +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/apps/meteor/tests/e2e/containers/saml/docker-compose.yml b/apps/meteor/tests/e2e/containers/saml/docker-compose.yml new file mode 100644 index 0000000000000..6d8a00f8eba9c --- /dev/null +++ b/apps/meteor/tests/e2e/containers/saml/docker-compose.yml @@ -0,0 +1,7 @@ +version: '3' +services: + testsamlidp_idp: + build: . + ports: + - "8080:8080" + - "8443:8443" diff --git a/apps/meteor/tests/e2e/fixtures/collections/users.ts b/apps/meteor/tests/e2e/fixtures/collections/users.ts index cc437597a5e01..5b85974fb57c2 100644 --- a/apps/meteor/tests/e2e/fixtures/collections/users.ts +++ b/apps/meteor/tests/e2e/fixtures/collections/users.ts @@ -1,7 +1,8 @@ import { faker } from '@faker-js/faker'; import type { IUser } from '@rocket.chat/core-typings'; -import type { IUserState } from '../userStates'; +import { DEFAULT_USER_CREDENTIALS } from '../../config/constants'; +import { type IUserState } from '../userStates'; type UserFixture = IUser & { username: string; @@ -23,7 +24,7 @@ export function createUserFixture(user: IUserState): UserFixture { utcOffset: -3, username, services: { - password: { bcrypt: '$2b$10$EMxaeQQbSw9JLL.YvOVPaOW8MKta6pgmp2BcN5Op4cC9bJiOqmUS.' }, + password: { bcrypt: DEFAULT_USER_CREDENTIALS.bcrypt }, email2fa: { enabled: true, changedAt: new Date() }, email: { verificationTokens: [ diff --git a/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts b/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts index 38835db4aaa6b..e7e68790cf3dd 100644 --- a/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts +++ b/apps/meteor/tests/e2e/fixtures/inject-initial-data.ts @@ -57,6 +57,22 @@ export default async function injectInitialData() { _id: 'API_Enable_Rate_Limiter_Dev', value: false, }, + { + _id: 'SAML_Custom_Default_provider', + value: 'test-sp', + }, + { + _id: 'SAML_Custom_Default_issuer', + value: 'http://localhost:3000/_saml/metadata/test-sp', + }, + { + _id: 'SAML_Custom_Default_entry_point', + value: 'http://localhost:8080/simplesaml/saml2/idp/SSOService.php', + }, + { + _id: 'SAML_Custom_Default_idp_slo_redirect_url', + value: 'http://localhost:8080/simplesaml/saml2/idp/SingleLogoutService.php', + }, { _id: 'Accounts_OAuth_Google', value: false, diff --git a/apps/meteor/tests/e2e/fixtures/userStates.ts b/apps/meteor/tests/e2e/fixtures/userStates.ts index 7bcab213f8fce..f21405a94f02e 100644 --- a/apps/meteor/tests/e2e/fixtures/userStates.ts +++ b/apps/meteor/tests/e2e/fixtures/userStates.ts @@ -86,6 +86,10 @@ export const Users = { user1: generateContext('user1'), user2: generateContext('user2'), user3: generateContext('user3'), + samluser1: generateContext('samluser1'), + samluser2: generateContext('samluser2'), + userForSamlMerge: generateContext('user_for_saml_merge'), + userForSamlMerge2: generateContext('user_for_saml_merge2'), admin: generateContext('rocketchat.internal.admin.test'), }; diff --git a/apps/meteor/tests/e2e/login.spec.ts b/apps/meteor/tests/e2e/login.spec.ts index 958f5120f1422..41710fffa203c 100644 --- a/apps/meteor/tests/e2e/login.spec.ts +++ b/apps/meteor/tests/e2e/login.spec.ts @@ -1,13 +1,16 @@ import { faker } from '@faker-js/faker'; -import { Registration } from './page-objects'; +import { DEFAULT_USER_CREDENTIALS } from './config/constants'; +import { Utils, Registration } from './page-objects'; import { test, expect } from './utils/test'; test.describe.parallel('Login', () => { let poRegistration: Registration; + let poUtils: Utils; test.beforeEach(async ({ page }) => { poRegistration = new Registration(page); + poUtils = new Utils(page); await page.goto('/home'); }); @@ -27,4 +30,24 @@ test.describe.parallel('Login', () => { await expect(poRegistration.inputPassword).toBeInvalid(); }); }); + + test('Login with valid username and password', async () => { + await test.step('expect successful login', async () => { + await poRegistration.username.type('user1'); + await poRegistration.inputPassword.type(DEFAULT_USER_CREDENTIALS.password); + await poRegistration.btnLogin.click(); + + await expect(poUtils.mainContent).toBeVisible(); + }); + }); + + test('Login with valid email and password', async () => { + await test.step('expect successful login', async () => { + await poRegistration.username.type('user1@email.com'); + await poRegistration.inputPassword.type(DEFAULT_USER_CREDENTIALS.password); + await poRegistration.btnLogin.click(); + + await expect(poUtils.mainContent).toBeVisible(); + }); + }); }); diff --git a/apps/meteor/tests/e2e/page-objects/auth.ts b/apps/meteor/tests/e2e/page-objects/auth.ts index d0a7e13d65055..98421f6461ab7 100644 --- a/apps/meteor/tests/e2e/page-objects/auth.ts +++ b/apps/meteor/tests/e2e/page-objects/auth.ts @@ -20,6 +20,10 @@ export class Registration { return this.page.locator('role=button[name="Login"]'); } + get btnLoginWithSaml(): Locator { + return this.page.locator('role=button[name="SAML"]'); + } + get btnLoginWithGoogle(): Locator { return this.page.locator('role=button[name="Sign in with Google"]'); } diff --git a/apps/meteor/tests/e2e/saml.spec.ts b/apps/meteor/tests/e2e/saml.spec.ts new file mode 100644 index 0000000000000..faa6b25710c28 --- /dev/null +++ b/apps/meteor/tests/e2e/saml.spec.ts @@ -0,0 +1,290 @@ +import child_process from 'child_process'; +import path from 'path'; + +import { Page } from '@playwright/test'; +import { v2 as compose } from 'docker-compose' +import { MongoClient } from 'mongodb'; + +import * as constants from './config/constants'; +import { createUserFixture } from './fixtures/collections/users'; +import { Users } from './fixtures/userStates'; +import { Registration } from './page-objects'; +import { getUserInfo } from './utils/getUserInfo'; +import { setSettingValueById } from './utils/setSettingValueById'; +import { test, expect } from './utils/test'; + +const resetTestData = async (cleanupOnly = false) => { + // Reset saml users' data on mongo in the beforeAll hook to allow re-running the tests within the same playwright session + // This is needed because those tests will modify this data and running them a second time would trigger different code paths + const connection = await MongoClient.connect(constants.URL_MONGODB); + + const usernamesToDelete = [Users.userForSamlMerge, Users.userForSamlMerge2, Users.samluser1, Users.samluser2].map(({ data: { username }}) => username); + await connection + .db() + .collection('users') + .deleteMany({ + username: { + $in: usernamesToDelete, + } + }); + + if (cleanupOnly) { + return; + } + + const usersFixtures = [Users.userForSamlMerge, Users.userForSamlMerge2].map((user) => createUserFixture(user)); + await Promise.all( + usersFixtures.map((user) => + connection.db().collection('users').updateOne({ username: user.username }, { $set: user }, { upsert: true }), + ), + ); + + await Promise.all( + [ + { + _id: 'SAML_Custom_Default_logout_behaviour', + value: 'SAML', + }, + { + _id: 'SAML_Custom_Default_immutable_property', + value: 'EMail', + }, + { + _id: 'SAML_Custom_Default_mail_overwrite', + value: false, + }, + { + _id: 'SAML_Custom_Default', + value: false, + }, + ].map((setting) => + connection + .db() + .collection('rocketchat_settings') + .updateOne({ _id: setting._id }, { $set: { value: setting.value } }), + ), + ); +}; + +test.describe('SAML', () => { + let poRegistration: Registration; + + const containerPath = path.join(__dirname, 'containers', 'saml'); + + test.beforeAll(async ({ api }) => { + await resetTestData(); + + // Only one setting updated through the API to avoid refreshing the service configurations several times + await expect((await setSettingValueById(api, 'SAML_Custom_Default', true)).status()).toBe(200); + + await compose.buildOne('testsamlidp_idp', { + cwd: containerPath, + }); + + await compose.upOne('testsamlidp_idp', { + cwd: containerPath, + }); + }); + + test.afterAll(async () => { + await compose.down({ + cwd: containerPath, + }); + + // the compose CLI doesn't have any way to remove images, so try to remove it with a direct call to the docker cli, but ignore errors if it fails. + try { + child_process.spawn('docker', ['rmi', 'saml-testsamlidp_idp'], { + cwd: containerPath, + }); + } catch { + // ignore errors here + } + + // Remove saml test users so they don't interfere with other tests + await resetTestData(true); + }); + + test.beforeEach(async ({ page }) => { + poRegistration = new Registration(page); + + await page.goto('/home'); + }); + + test('Login', async ({ page, api }) => { + await test.step('expect to have SAML login button available', async () => { + await expect(poRegistration.btnLoginWithSaml).toBeVisible({ timeout: 10000 }); + }); + + await test.step('expect to be redirected to the IdP for login', async () => { + await poRegistration.btnLoginWithSaml.click(); + + await expect(page).toHaveURL(/.*\/simplesaml\/module.php\/core\/loginuserpass.php.*/); + }); + + await test.step('expect to be redirected back on successful login', async () => { + await page.getByLabel('Username').fill('samluser1'); + await page.getByLabel('Password').fill('password'); + await page.locator('role=button[name="Login"]').click(); + + await expect(page).toHaveURL('/home'); + }); + + await test.step('expect user data to have been mapped to the correct fields', async () => { + const user = await getUserInfo(api, 'samluser1'); + + expect(user).toBeDefined(); + expect(user?.username).toBe('samluser1'); + expect(user?.name).toBe('Saml User 1'); + expect(user?.emails).toBeDefined(); + expect(user?.emails?.[0].address).toBe('samluser1@example.com'); + }); + }); + + const doLoginStep = async (page: Page, username: string) => { + await test.step('expect successful login', async () => { + await poRegistration.btnLoginWithSaml.click(); + // Redirect to Idp + await expect(page).toHaveURL(/.*\/simplesaml\/module.php\/core\/loginuserpass.php.*/); + + // Fill username and password + await page.getByLabel('Username').fill(username); + await page.getByLabel('Password').fill('password'); + await page.locator('role=button[name="Login"]').click(); + + // Redirect back to rocket.chat + await expect(page).toHaveURL('/home'); + + await expect(page.getByLabel('User Menu')).toBeVisible(); + }); + }; + + const doLogoutStep = async (page: Page) => { + await test.step('logout', async () => { + await page.getByLabel('User Menu').click(); + await page.locator('//*[contains(@class, "rcx-option__content") and contains(text(), "Logout")]').click(); + + await expect(page).toHaveURL('/home'); + await expect(page.getByLabel('User Menu')).not.toBeVisible(); + }); + }; + + test('Logout - Rocket.Chat only', async ({ page, api }) => { + await test.step('Configure logout to only logout from Rocket.Chat', async () => { + await expect((await setSettingValueById(api, 'SAML_Custom_Default_logout_behaviour', 'Local')).status()).toBe(200); + }); + + await doLoginStep(page, 'samluser1'); + await doLogoutStep(page); + + await test.step('expect IdP to redirect back automatically on new login request', async () => { + await poRegistration.btnLoginWithSaml.click(); + + await expect(page).toHaveURL('/home'); + }); + }); + + test('Logout - Single Sign Out', async ({ page, api }) => { + await test.step('Configure logout to terminate SAML session', async () => { + await expect((await setSettingValueById(api, 'SAML_Custom_Default_logout_behaviour', 'SAML')).status()).toBe(200); + }) + + await doLoginStep(page, 'samluser1'); + await doLogoutStep(page); + + await test.step('expect IdP to show login form on new login request', async () => { + await poRegistration.btnLoginWithSaml.click(); + + await expect(page).toHaveURL(/.*\/simplesaml\/module.php\/core\/loginuserpass.php.*/); + await expect(page.getByLabel('Username')).toBeVisible(); + }); + }); + + test('User Merge - By Email', async ({ page, api }) => { + await test.step('Configure SAML to identify users by email', async () => { + await expect((await setSettingValueById(api, 'SAML_Custom_Default_immutable_property', 'EMail')).status()).toBe(200); + }); + + await doLoginStep(page, 'samluser2'); + + await test.step('expect user data to have been mapped to the correct fields', async () => { + const user = await getUserInfo(api, 'samluser2'); + + expect(user).toBeDefined(); + expect(user?._id).toBe('user_for_saml_merge'); + expect(user?.username).toBe('samluser2'); + expect(user?.name).toBe('Saml User 2'); + expect(user?.emails).toBeDefined(); + expect(user?.emails?.[0].address).toBe('user_for_saml_merge@email.com'); + }); + }); + + test('User Merge - By Username', async ({ page, api }) => { + await test.step('Configure SAML to identify users by username', async () => { + await expect((await setSettingValueById(api, 'SAML_Custom_Default_immutable_property', 'Username')).status()).toBe(200); + await expect((await setSettingValueById(api, 'SAML_Custom_Default_mail_overwrite', false)).status()).toBe(200); + }); + + await doLoginStep(page, 'samluser3'); + + await test.step('expect user data to have been mapped to the correct fields', async () => { + const user = await getUserInfo(api, 'user_for_saml_merge2'); + + expect(user).toBeDefined(); + expect(user?._id).toBe('user_for_saml_merge2'); + expect(user?.username).toBe('user_for_saml_merge2'); + expect(user?.name).toBe('Saml User 3'); + expect(user?.emails).toBeDefined(); + expect(user?.emails?.[0].address).toBe('user_for_saml_merge2@email.com'); + }); + }); + + test('User Merge - By Username with Email Override', async ({ page, api }) => { + await test.step('Configure SAML to identify users by username', async () => { + await expect((await setSettingValueById(api, 'SAML_Custom_Default_immutable_property', 'Username')).status()).toBe(200); + await expect((await setSettingValueById(api, 'SAML_Custom_Default_mail_overwrite', true)).status()).toBe(200); + }); + + await doLoginStep(page, 'samluser3'); + + await test.step('expect user data to have been mapped to the correct fields', async () => { + const user = await getUserInfo(api, 'user_for_saml_merge2'); + + expect(user).toBeDefined(); + expect(user?._id).toBe('user_for_saml_merge2'); + expect(user?.username).toBe('user_for_saml_merge2'); + expect(user?.name).toBe('Saml User 3'); + expect(user?.emails).toBeDefined(); + expect(user?.emails?.[0].address).toBe('samluser3@example.com'); + }); + }); + + test.fixme('User Merge - By Custom Identifier', async () => { + // Test user merge with a custom identifier configured in the fieldmap + }); + + test.fixme('Signature Validation', async () => { + // Test login with signed responses + }); + + test.fixme('Login - User without username', async () => { + // Test login with a SAML user with no username + // Test different variations of the Immutable Property setting + }); + + test.fixme('Login - User without email', async () => { + // Test login with a SAML user with no email + // Test different variations of the Immutable Property setting + }); + + test.fixme('Login - User without name', async () => { + // Test login with a SAML user with no name + }); + + test.fixme('Login - User with channels attribute', async () => { + // Test login with a SAML user with a "channels" attribute + }); + + test.fixme('Data Sync - Custom Field Map', async () => { + // Test the data sync using a custom fieldmap setting + }); +}); diff --git a/apps/meteor/tests/e2e/utils/getUserInfo.ts b/apps/meteor/tests/e2e/utils/getUserInfo.ts new file mode 100644 index 0000000000000..13c592a7244b7 --- /dev/null +++ b/apps/meteor/tests/e2e/utils/getUserInfo.ts @@ -0,0 +1,15 @@ +import { IUser } from '@rocket.chat/core-typings'; + +import type { BaseTest } from './test'; + +export const getUserInfo = async (api: BaseTest['api'], username: string): Promise => { + const response = await api.get(`/users.info?username=${username}`); + + if (response.status() !== 200) { + throw new Error('Failed to get user info.'); + } + + const data = await response.json(); + + return data.user; +} diff --git a/yarn.lock b/yarn.lock index 645a843be1e01..7565bb974e76d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9889,6 +9889,7 @@ __metadata: date-fns: ^2.28.0 date.js: ~0.3.3 debug: ~4.1.1 + docker-compose: ^0.24.3 dompurify: ^2.3.8 ejson: ^2.2.3 emailreplyparser: ^0.0.5 @@ -21185,6 +21186,15 @@ __metadata: languageName: node linkType: hard +"docker-compose@npm:^0.24.3": + version: 0.24.3 + resolution: "docker-compose@npm:0.24.3" + dependencies: + yaml: ^2.2.2 + checksum: b2149eafb6e0a37ff4595044fe63d2fac23483afab06bca71cece78df4bae6f796b0a123854957addda77cc0559f205bc03cf3984ced816161e30e7f247d88e7 + languageName: node + linkType: hard + "doctrine@npm:^2.1.0": version: 2.1.0 resolution: "doctrine@npm:2.1.0" @@ -42556,6 +42566,13 @@ __metadata: languageName: node linkType: hard +"yaml@npm:^2.2.2": + version: 2.3.4 + resolution: "yaml@npm:2.3.4" + checksum: e6d1dae1c6383bcc8ba11796eef3b8c02d5082911c6723efeeb5ba50fc8e881df18d645e64de68e421b577296000bea9c75d6d9097c2f6699da3ae0406c030d8 + languageName: node + linkType: hard + "yamljs@npm:0.3.0": version: 0.3.0 resolution: "yamljs@npm:0.3.0"