Merge pull request #7644 from opf/feature/recaptcha

Implement recaptcha v2/v3 module

[ci skip]
pull/7649/head
Oliver Günther 5 years ago committed by GitHub
commit 600b831919
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      Gemfile.lock
  2. 1
      Gemfile.modules
  3. 104
      frontend/npm-shrinkwrap.json
  4. 2
      modules/recaptcha/Gemfile
  5. 38
      modules/recaptcha/app/controllers/recaptcha/admin_controller.rb
  6. 111
      modules/recaptcha/app/controllers/recaptcha/request_controller.rb
  7. 13
      modules/recaptcha/app/helpers/recaptcha_helper.rb
  8. 6
      modules/recaptcha/app/models/recaptcha/entry.rb
  9. 46
      modules/recaptcha/app/views/recaptcha/admin/show.html.erb
  10. 32
      modules/recaptcha/app/views/recaptcha/request/perform.html.erb
  11. 22
      modules/recaptcha/config/locales/en.yml
  12. 10
      modules/recaptcha/config/routes.rb
  13. 9
      modules/recaptcha/db/migrate/20190905130336_add_recaptcha_entries.rb
  14. 9
      modules/recaptcha/lib/open_project/recaptcha.rb
  15. 44
      modules/recaptcha/lib/open_project/recaptcha/engine.rb
  16. 5
      modules/recaptcha/lib/open_project/recaptcha/recaptcha.rb
  17. 1
      modules/recaptcha/lib/openproject-recaptcha.rb
  18. 16
      modules/recaptcha/openproject-recaptcha.gemspec
  19. 49
      modules/recaptcha/spec/controllers/admin_controller_spec.rb
  20. 48
      modules/recaptcha/spec/controllers/request_controller_spec.rb
  21. 2
      modules/recaptcha/spec/spec_helper.rb

@ -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!

@ -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'

@ -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
},

@ -0,0 +1,2 @@
source 'https://rubygems.org'
gemspec

@ -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

@ -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

@ -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

@ -0,0 +1,6 @@
module Recaptcha
class Entry < ::ApplicationRecord
self.table_name_prefix = 'recaptcha_'
belongs_to :user
end
end

@ -0,0 +1,46 @@
<% html_title(t(:label_administration), t('recaptcha.label_recaptcha')) -%>
<%= breadcrumb_toolbar t('recaptcha.label_recaptcha') %>
<section class="admin--edit-section">
<%= styled_form_tag({ action: :update },
method: :post,
id: 'update-recaptcha-settings-form') do %>
<fieldset class="form--fieldset">
<legend class="form--fieldset-legend"><%= t(:label_settings) %></legend>
<div class="form--field">
<label class="form--label" for='recaptcha_type'><%= t('recaptcha.settings.type') %></label>
<div class="form--field-container">
<%= styled_select_tag 'recaptcha_type',
options_for_select(recaptcha_available_options, Setting.plugin_openproject_recaptcha[:recaptcha_type]),
container_class: '-middle' %>
</div>
<div class="form--field-instructions">
<%= 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 %>
</div>
</div>
<div class="form--field">
<label class="form--label" for='website_key'><%= t('recaptcha.settings.website_key') %></label>
<div class="form--field-container">
<%= styled_text_field_tag 'website_key',
Setting.plugin_openproject_recaptcha[:website_key] %>
</div>
<div class="form--field-instructions">
<%= I18n.t('recaptcha.settings.website_key_text') %>
</div>
</div>
<div class="form--field">
<label class="form--label" for='secret_key'><%= t('recaptcha.settings.secret_key') %></label>
<div class="form--field-container">
<%= styled_text_field_tag 'secret_key',
Setting.plugin_openproject_recaptcha[:secret_key] %>
</div>
<div class="form--field-instructions">
<%= I18n.t('recaptcha.settings.secret_key_text') %>
</div>
</div>
</fieldset>
<%= styled_submit_tag l(:button_apply), class: '-highlight' %>
<% end %>
</section>

@ -0,0 +1,32 @@
<% html_title t('recaptcha.label_recaptcha') %>
<% breadcrumb_paths(t('recaptcha.label_recaptcha')) %>
<div id="login-form" class="form -bordered">
<%= styled_form_tag({ action: :verify }, { :autocomplete => "off", :id => 'submit_captcha' }) do %>
<h2><%= t 'recaptcha.verify_account' %></h2>
<% if recaptcha_settings[:recaptcha_type] == ::OpenProject::Recaptcha::TYPE_V2 %>
<input type="hidden" name="g-recaptcha-response" />
<%= 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] %>
<p><%= t('recaptcha.button_please_wait') %></p>
<%= nonced_javascript_tag do %>
function submitRecaptchaForm(id, val) {
document.getElementById(id).value = val;
document.getElementById('submit_captcha').submit();
}
<% end %>
<% end %>
<% end %>
</div>

@ -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.
<br/>
Please see the following link for more details on reCAPTCHA and their versions, and how
to create the website and secret keys: %{recaptcha_link}

@ -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

@ -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

@ -0,0 +1,9 @@
module OpenProject
module Recaptcha
TYPE_DISABLED ||= 'disabled'
TYPE_V2 ||= 'v2'
TYPE_V3 ||= 'v3'
require "open_project/recaptcha/engine"
end
end

@ -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

@ -0,0 +1,5 @@
module OpenProject
module Recaptcha
require "open_project/recaptcha/engine"
end
end

@ -0,0 +1 @@
require 'open_project/recaptcha'

@ -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

@ -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

@ -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

@ -0,0 +1,2 @@
# -- load spec_helper from OpenProject core
require "spec_helper"
Loading…
Cancel
Save