From 08e5f232fbc35af8c02a8b18f8e024577fe7abcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 5 Sep 2019 09:39:26 +0200 Subject: [PATCH] Implement recaptcha v2/v3 module --- Gemfile.lock | 9 ++ Gemfile.modules | 1 + frontend/npm-shrinkwrap.json | 104 ++++++++-------- modules/recaptcha/Gemfile | 2 + .../controllers/recaptcha/admin_controller.rb | 38 ++++++ .../recaptcha/request_controller.rb | 111 ++++++++++++++++++ .../recaptcha/app/helpers/recaptcha_helper.rb | 13 ++ .../recaptcha/app/models/recaptcha/entry.rb | 6 + .../app/views/recaptcha/admin/show.html.erb | 46 ++++++++ .../views/recaptcha/request/perform.html.erb | 32 +++++ modules/recaptcha/config/locales/en.yml | 22 ++++ modules/recaptcha/config/routes.rb | 10 ++ .../20190905130336_add_recaptcha_entries.rb | 9 ++ .../recaptcha/lib/open_project/recaptcha.rb | 9 ++ .../lib/open_project/recaptcha/engine.rb | 44 +++++++ .../lib/open_project/recaptcha/recaptcha.rb | 5 + .../recaptcha/lib/openproject-recaptcha.rb | 1 + .../recaptcha/openproject-recaptcha.gemspec | 16 +++ .../spec/controllers/admin_controller_spec.rb | 49 ++++++++ .../controllers/request_controller_spec.rb | 48 ++++++++ modules/recaptcha/spec/spec_helper.rb | 2 + 21 files changed, 525 insertions(+), 52 deletions(-) create mode 100644 modules/recaptcha/Gemfile create mode 100644 modules/recaptcha/app/controllers/recaptcha/admin_controller.rb create mode 100644 modules/recaptcha/app/controllers/recaptcha/request_controller.rb create mode 100644 modules/recaptcha/app/helpers/recaptcha_helper.rb create mode 100644 modules/recaptcha/app/models/recaptcha/entry.rb create mode 100644 modules/recaptcha/app/views/recaptcha/admin/show.html.erb create mode 100644 modules/recaptcha/app/views/recaptcha/request/perform.html.erb create mode 100644 modules/recaptcha/config/locales/en.yml create mode 100644 modules/recaptcha/config/routes.rb create mode 100644 modules/recaptcha/db/migrate/20190905130336_add_recaptcha_entries.rb create mode 100644 modules/recaptcha/lib/open_project/recaptcha.rb create mode 100644 modules/recaptcha/lib/open_project/recaptcha/engine.rb create mode 100644 modules/recaptcha/lib/open_project/recaptcha/recaptcha.rb create mode 100644 modules/recaptcha/lib/openproject-recaptcha.rb create mode 100644 modules/recaptcha/openproject-recaptcha.gemspec create mode 100644 modules/recaptcha/spec/controllers/admin_controller_spec.rb create mode 100644 modules/recaptcha/spec/controllers/request_controller_spec.rb create mode 100644 modules/recaptcha/spec/spec_helper.rb diff --git a/Gemfile.lock b/Gemfile.lock index 2f187abb57..d069e6e7e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -205,6 +205,12 @@ PATH pdf-inspector (~> 1.3.0) prawn (~> 2.2) +PATH + remote: modules/recaptcha + specs: + openproject-recaptcha (1.0.0) + recaptcha (~> 5.1.0) + PATH remote: modules/reporting_engine specs: @@ -716,6 +722,8 @@ GEM rake (12.3.2) rbtree (0.4.2) rdoc (6.1.1) + recaptcha (5.1.0) + json redcarpet (3.4.0) reform (2.2.4) disposable (>= 0.4.1) @@ -984,6 +992,7 @@ DEPENDENCIES openproject-meeting! openproject-openid_connect! openproject-pdf_export! + openproject-recaptcha! openproject-reporting! openproject-token (~> 1.0.1) openproject-translations! diff --git a/Gemfile.modules b/Gemfile.modules index 3e10108320..0fdf6b5037 100644 --- a/Gemfile.modules +++ b/Gemfile.modules @@ -37,6 +37,7 @@ group :opf_plugins do gem 'openproject-webhooks', path: 'modules/webhooks' gem 'openproject-github_integration', path: 'modules/github_integration' gem 'openproject-ldap_groups', path: 'modules/ldap_groups' + gem 'openproject-recaptcha', path: 'modules/recaptcha' gem 'grids', path: 'modules/grids' gem 'my_page', path: 'modules/my_page' diff --git a/frontend/npm-shrinkwrap.json b/frontend/npm-shrinkwrap.json index 986612e1d5..818a66c3eb 100644 --- a/frontend/npm-shrinkwrap.json +++ b/frontend/npm-shrinkwrap.json @@ -1507,7 +1507,7 @@ }, "@types/q": { "version": "0.0.32", - "resolved": "http://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", + "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=", "dev": true }, @@ -2204,7 +2204,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "requires": { "ansi-styles": "^2.2.1", @@ -2570,7 +2570,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "requires": { "buffer-xor": "^1.0.3", @@ -2806,7 +2806,7 @@ }, "camelcase-keys": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "requires": { "camelcase": "^2.0.0", @@ -2884,7 +2884,7 @@ "dependencies": { "color-convert": { "version": "0.5.3", - "resolved": "http://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=" } } @@ -3115,7 +3115,7 @@ }, "colors": { "version": "1.1.2", - "resolved": "http://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", "dev": true }, @@ -3400,7 +3400,7 @@ }, "create-hash": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "requires": { "cipher-base": "^1.0.1", @@ -3412,7 +3412,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "requires": { "cipher-base": "^1.0.3", @@ -3520,7 +3520,7 @@ }, "d": { "version": "0.1.1", - "resolved": "http://registry.npmjs.org/d/-/d-0.1.1.tgz", + "resolved": "https://registry.npmjs.org/d/-/d-0.1.1.tgz", "integrity": "sha1-2hhMU10Y2O57oqoim5FACfrhEwk=", "requires": { "es5-ext": "~0.10.2" @@ -3784,7 +3784,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "requires": { "bn.js": "^4.1.0", @@ -4001,7 +4001,7 @@ }, "engine.io-client": { "version": "3.2.1", - "resolved": "http://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", "dev": true, "requires": { @@ -4124,7 +4124,7 @@ "dependencies": { "d": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/d/-/d-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", "requires": { "es5-ext": "^0.10.9" @@ -4139,7 +4139,7 @@ }, "es6-promisify": { "version": "5.0.0", - "resolved": "http://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", "requires": { "es6-promise": "^4.0.3" @@ -4161,7 +4161,7 @@ "dependencies": { "d": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/d/-/d-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", "requires": { "es5-ext": "^0.10.9" @@ -4259,7 +4259,7 @@ "dependencies": { "d": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/d/-/d-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", "requires": { "es5-ext": "^0.10.9" @@ -5123,7 +5123,7 @@ "dependencies": { "async": { "version": "1.5.0", - "resolved": "http://registry.npmjs.org/async/-/async-1.5.0.tgz", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.0.tgz", "integrity": "sha1-J5ZkJyNXOFlWVjP8YnRES+4vjOM=" } } @@ -5939,7 +5939,7 @@ }, "fast-deep-equal": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" }, "json-schema-traverse": { @@ -6088,13 +6088,13 @@ "dependencies": { "commander": { "version": "0.6.1", - "resolved": "http://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", "dev": true }, "mkdirp": { "version": "0.3.5", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", "dev": true } @@ -6200,7 +6200,7 @@ }, "jsesc": { "version": "1.3.0", - "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" }, "json-parse-better-errors": { @@ -6595,7 +6595,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "requires": { "graceful-fs": "^4.1.2", @@ -6607,7 +6607,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" } } @@ -6873,7 +6873,7 @@ }, "media-typer": { "version": "0.3.0", - "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "mem": { @@ -6902,7 +6902,7 @@ "dependencies": { "next-tick": { "version": "0.2.2", - "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-0.2.2.tgz", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-0.2.2.tgz", "integrity": "sha1-ddpKkn7liH45BliABltzNkE7MQ0=" } } @@ -6918,7 +6918,7 @@ }, "meow": { "version": "3.7.0", - "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "requires": { "camelcase-keys": "^2.0.0", @@ -7191,7 +7191,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" @@ -7221,7 +7221,7 @@ "dependencies": { "readdirp": { "version": "0.2.5", - "resolved": "http://registry.npmjs.org/readdirp/-/readdirp-0.2.5.tgz", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-0.2.5.tgz", "integrity": "sha1-xMJ25Sl3riXbUZH+UdAIVQ8V2bs=", "dev": true, "requires": { @@ -7307,7 +7307,7 @@ }, "next-tick": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, "ng-dynamic-component": { @@ -7378,7 +7378,7 @@ "dependencies": { "jsesc": { "version": "0.5.0", - "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" } } @@ -7429,7 +7429,7 @@ "dependencies": { "semver": { "version": "5.3.0", - "resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" }, "tar": { @@ -7520,7 +7520,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "requires": { "ansi-styles": "^2.2.1", @@ -7876,7 +7876,7 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-locale": { @@ -7891,7 +7891,7 @@ }, "os-tmpdir": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { @@ -8129,7 +8129,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { @@ -8381,7 +8381,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -8409,7 +8409,7 @@ }, "globby": { "version": "5.0.0", - "resolved": "http://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", "dev": true, "requires": { @@ -8429,7 +8429,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, @@ -8692,7 +8692,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" } } @@ -8741,7 +8741,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" } } @@ -8776,7 +8776,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -9049,7 +9049,7 @@ }, "safe-regex": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "requires": { "ret": "~0.1.10" @@ -9233,7 +9233,7 @@ "dependencies": { "source-map": { "version": "0.4.4", - "resolved": "http://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", "requires": { "amdefine": ">=0.0.4" @@ -9360,7 +9360,7 @@ }, "serialize-error": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", "integrity": "sha1-ULZ51WNc34Rme9yOWa9OW4HV9go=" }, "serialize-javascript": { @@ -9437,7 +9437,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "requires": { "inherits": "^2.0.1", @@ -9667,7 +9667,7 @@ }, "socket.io-parser": { "version": "3.2.0", - "resolved": "http://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", "dev": true, "requires": { @@ -10109,7 +10109,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -10117,7 +10117,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" @@ -10133,7 +10133,7 @@ }, "strip-eof": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, "strip-indent": { @@ -10314,7 +10314,7 @@ }, "through": { "version": "2.3.8", - "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { @@ -10452,7 +10452,7 @@ }, "promise": { "version": "2.0.0", - "resolved": "http://registry.npmjs.org/promise/-/promise-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/promise/-/promise-2.0.0.tgz", "integrity": "sha1-RmSKqdYFr10ucMMCS/WUNtoCuA4=", "dev": true, "requires": { @@ -12431,7 +12431,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "requires": { "string-width": "^1.0.1", @@ -12494,7 +12494,7 @@ }, "xmlbuilder": { "version": "9.0.7", - "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", "dev": true }, diff --git a/modules/recaptcha/Gemfile b/modules/recaptcha/Gemfile new file mode 100644 index 0000000000..851fabc21d --- /dev/null +++ b/modules/recaptcha/Gemfile @@ -0,0 +1,2 @@ +source 'https://rubygems.org' +gemspec diff --git a/modules/recaptcha/app/controllers/recaptcha/admin_controller.rb b/modules/recaptcha/app/controllers/recaptcha/admin_controller.rb new file mode 100644 index 0000000000..302a5b201f --- /dev/null +++ b/modules/recaptcha/app/controllers/recaptcha/admin_controller.rb @@ -0,0 +1,38 @@ +module ::Recaptcha + class AdminController < ApplicationController + include ::RecaptchaHelper + + before_action :require_admin + before_action :validate_settings, only: :update + layout 'admin' + + menu_item :plugin_recaptcha + + def show; end + + def update + Setting.plugin_openproject_recaptcha = @settings + flash[:notice] = I18n.t(:notice_successful_update) + redirect_to action: :show + end + + private + + def validate_settings + new_params = permitted_params + allowed_options = recaptcha_available_options.map(&:last) + + unless allowed_options.include? new_params[:recaptcha_type] + flash[:error] = I18n.t(:error_code, code: '400') + redirect_to action: :show + return + end + + @settings = new_params.to_h.symbolize_keys + end + + def permitted_params + params.permit(:recaptcha_type, :website_key, :secret_key) + end + end +end diff --git a/modules/recaptcha/app/controllers/recaptcha/request_controller.rb b/modules/recaptcha/app/controllers/recaptcha/request_controller.rb new file mode 100644 index 0000000000..1e0f84c60b --- /dev/null +++ b/modules/recaptcha/app/controllers/recaptcha/request_controller.rb @@ -0,0 +1,111 @@ +require 'recaptcha' + +module ::Recaptcha + class RequestController < ApplicationController + # Include global layout helper + layout 'no_menu' + + # User is not yet logged in, so skip login required check + skip_before_action :check_if_login_required + + # Skip if recaptcha was disabled + before_action :skip_if_disabled + + # Require authenticated user from the core to be present + before_action :require_authenticated_user + + # Skip if user has confirmed already + before_action :skip_if_user_verified + + ## + # Request verification form + def perform + use_content_security_policy_named_append(:recaptcha) + end + + def verify + if valid_recaptcha? + save_recpatcha_verification_success! + complete_stage_redirect + else + fail_recaptcha I18n.t('recaptcha.error_captcha') + end + end + + private + + ## + # Insert that the account was verified + def save_recpatcha_verification_success! + # Remove all previous + ::Recaptcha::Entry.where(user_id: @authenticated_user.id).delete_all + ::Recaptcha::Entry.create!(user_id: @authenticated_user.id, version: recaptcha_version) + end + + def recaptcha_version + case recaptcha_settings[:recaptcha_type] + when ::OpenProject::Recaptcha::TYPE_DISABLED + 0 + when ::OpenProject::Recaptcha::TYPE_V2 + 2 + when ::OpenProject::Recaptcha::TYPE_V3 + 3 + end + end + + ## + # + def valid_recaptcha? + call_args = { secret_key: recaptcha_settings[:secret_key] } + if recaptcha_version == 3 + call_args[:action] = 'login' + end + + verify_recaptcha call_args + end + + ## + # fail the recaptcha + def fail_recaptcha(msg) + flash[:error] = msg + failure_stage_redirect + end + + ## + # Ensure the authentication stage from the core provided the authenticated user + def require_authenticated_user + @authenticated_user = User.find(session[:authenticated_user_id]) + rescue ActiveRecord::RecordNotFound + Rails.logger.error "Failed to find authenticated_user for recaptcha verify." + failure_stage_redirect + end + + def recaptcha_settings + Setting.plugin_openproject_recaptcha + end + + def skip_if_disabled + if recaptcha_settings[:recaptcha_type] == ::OpenProject::Recaptcha::TYPE_DISABLED + complete_stage_redirect + end + end + + def skip_if_user_verified + if ::Recaptcha::Entry.where(user_id: @authenticated_user.id).exists? + Rails.logger.debug { "User #{@authenticated_user.id} already provided recaptcha. Skipping. " } + complete_stage_redirect + end + end + + ## + # Complete this authentication step and return to core + # logging in the user + def complete_stage_redirect + redirect_to authentication_stage_complete_path :recaptcha + end + + def failure_stage_redirect + redirect_to authentication_stage_failure_path :recaptcha + end + end +end diff --git a/modules/recaptcha/app/helpers/recaptcha_helper.rb b/modules/recaptcha/app/helpers/recaptcha_helper.rb new file mode 100644 index 0000000000..8c8aa2e2bc --- /dev/null +++ b/modules/recaptcha/app/helpers/recaptcha_helper.rb @@ -0,0 +1,13 @@ +module RecaptchaHelper + def recaptcha_available_options + [ + [I18n.t('recaptcha.settings.type_disabled'), ::OpenProject::Recaptcha::TYPE_DISABLED], + [I18n.t('recaptcha.settings.type_v2'), ::OpenProject::Recaptcha::TYPE_V2], + [I18n.t('recaptcha.settings.type_v3'), ::OpenProject::Recaptcha::TYPE_V3] + ] + end + + def recaptcha_settings + Setting.plugin_openproject_recaptcha + end +end diff --git a/modules/recaptcha/app/models/recaptcha/entry.rb b/modules/recaptcha/app/models/recaptcha/entry.rb new file mode 100644 index 0000000000..3af1e68c1e --- /dev/null +++ b/modules/recaptcha/app/models/recaptcha/entry.rb @@ -0,0 +1,6 @@ +module Recaptcha + class Entry < ::ApplicationRecord + self.table_name_prefix = 'recaptcha_' + belongs_to :user + end +end diff --git a/modules/recaptcha/app/views/recaptcha/admin/show.html.erb b/modules/recaptcha/app/views/recaptcha/admin/show.html.erb new file mode 100644 index 0000000000..77079db7b5 --- /dev/null +++ b/modules/recaptcha/app/views/recaptcha/admin/show.html.erb @@ -0,0 +1,46 @@ +<% html_title(t(:label_administration), t('recaptcha.label_recaptcha')) -%> + +<%= breadcrumb_toolbar t('recaptcha.label_recaptcha') %> + +
+ <%= styled_form_tag({ action: :update }, + method: :post, + id: 'update-recaptcha-settings-form') do %> +
+ <%= t(:label_settings) %> +
+ +
+ <%= styled_select_tag 'recaptcha_type', + options_for_select(recaptcha_available_options, Setting.plugin_openproject_recaptcha[:recaptcha_type]), + container_class: '-middle' %> +
+
+ <%= I18n.t('recaptcha.settings.recaptcha_description_html', + recaptcha_link: link_to('https://www.google.com/recaptcha', 'https://www.google.com/recaptcha', target: '_blank')).html_safe %> +
+
+
+ +
+ <%= styled_text_field_tag 'website_key', + Setting.plugin_openproject_recaptcha[:website_key] %> +
+
+ <%= I18n.t('recaptcha.settings.website_key_text') %> +
+
+
+ +
+ <%= styled_text_field_tag 'secret_key', + Setting.plugin_openproject_recaptcha[:secret_key] %> +
+
+ <%= I18n.t('recaptcha.settings.secret_key_text') %> +
+
+
+ <%= styled_submit_tag l(:button_apply), class: '-highlight' %> + <% end %> +
diff --git a/modules/recaptcha/app/views/recaptcha/request/perform.html.erb b/modules/recaptcha/app/views/recaptcha/request/perform.html.erb new file mode 100644 index 0000000000..a23c7ed446 --- /dev/null +++ b/modules/recaptcha/app/views/recaptcha/request/perform.html.erb @@ -0,0 +1,32 @@ +<% html_title t('recaptcha.label_recaptcha') %> +<% breadcrumb_paths(t('recaptcha.label_recaptcha')) %> +
+ <%= styled_form_tag({ action: :verify }, { :autocomplete => "off", :id => 'submit_captcha' }) do %> +

<%= t 'recaptcha.verify_account' %>

+ <% if recaptcha_settings[:recaptcha_type] == ::OpenProject::Recaptcha::TYPE_V2 %> + + <%= recaptcha_tags nonce: content_security_policy_script_nonce, + callback: 'submitRecaptchaForm', + site_key: recaptcha_settings[:website_key] %> + <%= nonced_javascript_tag do %> + function submitRecaptchaForm(val) { + document.getElementById('g-recaptcha-response').value = val; + document.getElementById('submit_captcha').submit(); + } + <% end %> + <% elsif recaptcha_settings[:recaptcha_type] == ::OpenProject::Recaptcha::TYPE_V3 %> + <%= recaptcha_v3 action: 'login', + nonce: content_security_policy_script_nonce, + callback: 'submitRecaptchaForm', + site_key: recaptcha_settings[:website_key] %> + +

<%= t('recaptcha.button_please_wait') %>

+ <%= nonced_javascript_tag do %> + function submitRecaptchaForm(id, val) { + document.getElementById(id).value = val; + document.getElementById('submit_captcha').submit(); + } + <% end %> + <% end %> + <% end %> +
diff --git a/modules/recaptcha/config/locales/en.yml b/modules/recaptcha/config/locales/en.yml new file mode 100644 index 0000000000..ba52a0cc0a --- /dev/null +++ b/modules/recaptcha/config/locales/en.yml @@ -0,0 +1,22 @@ +# English strings go here for Rails i18n +en: + recaptcha: + label_recaptcha: "reCAPTCHA" + button_please_wait: 'Please wait ...' + verify_account: "Verify your account" + error_captcha: "You're account could not be verified. Please contact an administrator." + settings: + website_key: 'Website key' + website_key_text: 'Enter the website key you created on the reCAPTCHA admin console for this domain.' + secret_key: 'Secret key' + secret_key_text: 'Enter the secret key you created on the reCAPTCHA admin console.' + type: 'Use reCAPTCHA' + type_disabled: 'Disable reCAPTCHA' + type_v2: 'reCAPTCHA v2' + type_v3: 'reCAPTCHA v3' + recaptcha_description_html: > + reCAPTCHA is a free service by Google that can be enabled for your OpenProject instance. + If enabled, a captcha form will be rendered upon login for all users that have not verified a captcha yet. +
+ Please see the following link for more details on reCAPTCHA and their versions, and how + to create the website and secret keys: %{recaptcha_link} diff --git a/modules/recaptcha/config/routes.rb b/modules/recaptcha/config/routes.rb new file mode 100644 index 0000000000..eb86a6435f --- /dev/null +++ b/modules/recaptcha/config/routes.rb @@ -0,0 +1,10 @@ +OpenProject::Application::routes.draw do + + namespace 'recaptcha' do + get :settings, to: 'admin#show' + post :settings, to: 'admin#update' + + get :request, to: 'request#perform', as: 'request' + post :verify, to: 'request#verify', as: 'verify' + end +end diff --git a/modules/recaptcha/db/migrate/20190905130336_add_recaptcha_entries.rb b/modules/recaptcha/db/migrate/20190905130336_add_recaptcha_entries.rb new file mode 100644 index 0000000000..e78b96e0c5 --- /dev/null +++ b/modules/recaptcha/db/migrate/20190905130336_add_recaptcha_entries.rb @@ -0,0 +1,9 @@ +class AddRecaptchaEntries < ActiveRecord::Migration[5.2] + def change + create_table :recaptcha_entries, id: :integer do |t| + t.references :user, index: true, foreign_key: { on_delete: :cascade } + t.timestamps + t.integer :version, null: false + end + end +end diff --git a/modules/recaptcha/lib/open_project/recaptcha.rb b/modules/recaptcha/lib/open_project/recaptcha.rb new file mode 100644 index 0000000000..8d6e3f12ec --- /dev/null +++ b/modules/recaptcha/lib/open_project/recaptcha.rb @@ -0,0 +1,9 @@ +module OpenProject + module Recaptcha + TYPE_DISABLED ||= 'disabled' + TYPE_V2 ||= 'v2' + TYPE_V3 ||= 'v3' + + require "open_project/recaptcha/engine" + end +end diff --git a/modules/recaptcha/lib/open_project/recaptcha/engine.rb b/modules/recaptcha/lib/open_project/recaptcha/engine.rb new file mode 100644 index 0000000000..540945c236 --- /dev/null +++ b/modules/recaptcha/lib/open_project/recaptcha/engine.rb @@ -0,0 +1,44 @@ +require 'open_project/plugins' +require 'recaptcha' + +module OpenProject::Recaptcha + class Engine < ::Rails::Engine + engine_name :openproject_recaptcha + + include OpenProject::Plugins::ActsAsOpEngine + + register 'openproject-recaptcha', + author_url: 'http://openproject.com', + settings: { + default: { + recaptcha_type: ::OpenProject::Recaptcha::TYPE_DISABLED + } + }, + bundled: true do + menu :admin_menu, + :plugin_recaptcha, + { controller: '/recaptcha/admin', action: :show }, + parent: :authentication, + caption: ->(*) { I18n.t('recaptcha.label_recaptcha') } + end + + config.after_initialize do + OpenProject::Authentication::Stage.register(:recaptcha, + nil, + run_after_activation: true, + active: -> { + type = Setting.plugin_openproject_recaptcha[:recaptcha_type] + type.present? && type.to_s != ::OpenProject::Recaptcha::TYPE_DISABLED + }) do + recaptcha_request_path + end + end + + config.to_prepare do + SecureHeaders::Configuration.named_append(:recaptcha) do |request| + { frame_src: %w(https://www.google.com/recaptcha/) } + end + + end + end +end diff --git a/modules/recaptcha/lib/open_project/recaptcha/recaptcha.rb b/modules/recaptcha/lib/open_project/recaptcha/recaptcha.rb new file mode 100644 index 0000000000..2b71a7a23b --- /dev/null +++ b/modules/recaptcha/lib/open_project/recaptcha/recaptcha.rb @@ -0,0 +1,5 @@ +module OpenProject + module Recaptcha + require "open_project/recaptcha/engine" + end +end diff --git a/modules/recaptcha/lib/openproject-recaptcha.rb b/modules/recaptcha/lib/openproject-recaptcha.rb new file mode 100644 index 0000000000..f626d3d0e5 --- /dev/null +++ b/modules/recaptcha/lib/openproject-recaptcha.rb @@ -0,0 +1 @@ +require 'open_project/recaptcha' diff --git a/modules/recaptcha/openproject-recaptcha.gemspec b/modules/recaptcha/openproject-recaptcha.gemspec new file mode 100644 index 0000000000..5586368280 --- /dev/null +++ b/modules/recaptcha/openproject-recaptcha.gemspec @@ -0,0 +1,16 @@ +# encoding: UTF-8 + +Gem::Specification.new do |s| + s.name = "openproject-recaptcha" + s.version = '1.0.0' + s.authors = "OpenProject GmbH" + s.email = "info@openproject.com" + s.homepage = "https://community.openproject.org/" + s.summary = "OpenProject ReCaptcha" + s.description = "This module provides recaptcha checks during login" + + s.files = Dir["{app,config,db,lib}/**/*", "CHANGELOG.md", "README.rdoc"] + s.test_files = Dir["spec/**/*"] + + s.add_dependency 'recaptcha', '~> 5.1.0' +end diff --git a/modules/recaptcha/spec/controllers/admin_controller_spec.rb b/modules/recaptcha/spec/controllers/admin_controller_spec.rb new file mode 100644 index 0000000000..5a068f05b2 --- /dev/null +++ b/modules/recaptcha/spec/controllers/admin_controller_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe ::Recaptcha::AdminController, type: :controller do + let(:user) { FactoryBot.build_stubbed :admin } + before do + login_as user + end + + describe 'as non admin' do + let(:user) { FactoryBot.build_stubbed :user } + + it 'does not allow access' do + get :show + expect(response.status).to eq 403 + + post :update + expect(response.status).to eq 403 + end + end + + describe 'show' do + it 'renders show' do + get :show + expect(response).to be_successful + expect(response).to render_template 'recaptcha/admin/show' + end + end + + describe '#update' do + it 'fails if invalid param' do + post :update, params: { recaptcha_type: :unknown } + expect(response).to be_redirect + expect(flash[:error]).to be_present + end + + it 'succeeds' do + expected = { recaptcha_type: 'v2', website_key: 'B', secret_key: 'A' } + + expect(Setting) + .to receive(:plugin_openproject_recaptcha=) + .with(expected) + + post :update, params: expected + expect(response).to be_redirect + expect(flash[:error]).to be_nil + expect(flash[:notice]).to be_present + end + end +end diff --git a/modules/recaptcha/spec/controllers/request_controller_spec.rb b/modules/recaptcha/spec/controllers/request_controller_spec.rb new file mode 100644 index 0000000000..7d12dde453 --- /dev/null +++ b/modules/recaptcha/spec/controllers/request_controller_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe ::Recaptcha::RequestController, type: :controller do + let(:user) { FactoryBot.create :user } + before do + login_as user + allow(Setting) + .to receive(:plugin_openproject_recaptcha) + .and_return(recaptcha_type: 'v2', website_key: 'A', secret_key: 'B') + + session[:authenticated_user_id] = user.id + session[:stage_secrets] = { recaptcha: 'asdf' } + end + + describe 'request' do + it 'renders the template' do + get :perform + expect(response).to be_successful + expect(response).to render_template 'recaptcha/request/perform' + end + + it 'skips if user is verified' do + allow(::Recaptcha::Entry) + .to receive_message_chain(:where, :exists?) + .and_return true + + get :perform + expect(response).to redirect_to stage_success_path(stage: :recaptcha, secret: 'asdf') + end + end + + describe 'verify' do + it 'succeeds assuming verification works' do + allow(@controller).to receive(:valid_recaptcha?).and_return true + expect(@controller).to receive(:save_recpatcha_verification_success!) + post :verify + expect(flash[:error]).to be_nil + expect(response).to redirect_to stage_success_path(stage: :recaptcha, secret: 'asdf') + end + + it 'fails assuming verification fails' do + allow(@controller).to receive(:valid_recaptcha?).and_return false + post :verify + expect(flash[:error]).to be_present + expect(response).to redirect_to stage_failure_path(stage: :recaptcha) + end + end +end diff --git a/modules/recaptcha/spec/spec_helper.rb b/modules/recaptcha/spec/spec_helper.rb new file mode 100644 index 0000000000..4351818bd6 --- /dev/null +++ b/modules/recaptcha/spec/spec_helper.rb @@ -0,0 +1,2 @@ +# -- load spec_helper from OpenProject core +require "spec_helper"