diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 212d5dcd..a4855e50 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -60,13 +60,13 @@ jobs: USER_INPUT_ENVIRONMENT=${{ inputs.environment }} echo "TARGET=${USER_INPUT_ENVIRONMENT:-staging}" >> $GITHUB_ENV # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7.6 # Not needed with a .ruby-version file + ruby-version: 2.7.8 # Not needed with a .ruby-version file bundler-cache: true # runs 'bundle install' and caches installed gems automatically - name: get-deployment-config - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: ${{ secrets.CONFIG_REPO }} # repository containing deployment settings token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT @@ -79,7 +79,7 @@ jobs: mkdir -p ~/.ssh ssh-keyscan -H ${{ secrets.SSH_JUMPHOST }} > ~/.ssh/known_hosts shell: bash - - uses: miloserdow/capistrano-deploy@master + - uses: miloserdow/capistrano-deploy@v3 with: target: ${{ env.TARGET }} # which environment to deploy deploy_key: ${{ secrets.DEPLOY_ENC_KEY }} # Name of the variable configured in Settings/Secrets of your github project diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index a3f95c93..358ff4f1 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -10,28 +10,28 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out the repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Log in to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: bioportal/ontologies_api - name: Build and push Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/ruby-unit-tests.yml b/.github/workflows/ruby-unit-tests.yml index 13dd5c3f..d764159b 100644 --- a/.github/workflows/ruby-unit-tests.yml +++ b/.github/workflows/ruby-unit-tests.yml @@ -12,7 +12,7 @@ jobs: backend: ['api', 'api-agraph'] # api runs tests with 4store backend and api-agraph runs with AllegroGraph backend runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build docker-compose run: docker-compose --profile 4store build #profile flag is set in order to build all containers in this step - name: Run unit tests @@ -22,8 +22,9 @@ jobs: ci_env=`bash <(curl -s https://codecov.io/env)` docker-compose run $ci_env -e CI --rm ${{ matrix.backend }} bundle exec rake test TESTOPTS='-v' - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: + token: ${{ secrets.CODECOV_TOKEN }} flags: unittests verbose: true fail_ci_if_error: false # optional (default = false) diff --git a/Gemfile.lock b/Gemfile.lock index f56d0232..392dbbe8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -26,7 +26,7 @@ GIT GIT remote: https://github.com/ncbo/ncbo_cron.git - revision: b01a9046c4c110f00e832e8c16073a74558fdba5 + revision: 46bd8e7eb7cfde6d173bdf55808a8e28d6700f1e branch: master specs: ncbo_cron (0.0.1) @@ -53,7 +53,7 @@ GIT GIT remote: https://github.com/ncbo/ontologies_linked_data.git - revision: ee0013f0ee23876076bff9d9258b46371ec3b248 + revision: 0423a4559b9cf6f176a521a7c78471938c8f754e branch: master specs: ontologies_linked_data (0.0.1) @@ -127,7 +127,7 @@ GEM capistrano (~> 3.1) sshkit (~> 1.3) coderay (1.1.3) - concurrent-ruby (1.2.2) + concurrent-ruby (1.2.3) connection_pool (2.4.1) cube-ruby (0.0.3) dante (0.2.0) @@ -153,16 +153,16 @@ GEM grpc (~> 1.59) get_process_mem (0.2.7) ffi (~> 1.0) - google-analytics-data (0.4.0) - google-analytics-data-v1beta (>= 0.7, < 2.a) + google-analytics-data (0.5.0) + google-analytics-data-v1beta (>= 0.11, < 2.a) google-cloud-core (~> 1.6) - google-analytics-data-v1beta (0.11.1) + google-analytics-data-v1beta (0.11.2) gapic-common (>= 0.21.1, < 2.a) google-cloud-errors (~> 1.0) google-cloud-core (1.6.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (2.1.0) + google-cloud-env (2.1.1) faraday (>= 1.0, < 3.a) google-cloud-errors (1.3.1) google-protobuf (3.25.2-aarch64-linux) @@ -175,23 +175,23 @@ GEM grpc (~> 1.27) googleapis-common-protos-types (1.11.0) google-protobuf (~> 3.18) - googleauth (1.9.1) + googleauth (1.10.0) faraday (>= 1.0, < 3.a) google-cloud-env (~> 2.1) jwt (>= 1.4, < 3.0) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) - grpc (1.60.0-aarch64-linux) + grpc (1.61.0-aarch64-linux) google-protobuf (~> 3.25) googleapis-common-protos-types (~> 1.0) - grpc (1.60.0-arm64-darwin) + grpc (1.61.0-arm64-darwin) google-protobuf (~> 3.25) googleapis-common-protos-types (~> 1.0) - grpc (1.60.0-x86_64-darwin) + grpc (1.61.0-x86_64-darwin) google-protobuf (~> 3.25) googleapis-common-protos-types (~> 1.0) - grpc (1.60.0-x86_64-linux) + grpc (1.61.0-x86_64-linux) google-protobuf (~> 3.25) googleapis-common-protos-types (~> 1.0) haml (5.2.2) @@ -221,7 +221,7 @@ GEM method_source (1.0.0) mime-types (3.5.2) mime-types-data (~> 3.2015) - mime-types-data (3.2023.1205) + mime-types-data (3.2024.0206) mini_mime (1.1.5) minitest (4.7.5) minitest-stub_any_instance (1.0.3) @@ -230,7 +230,7 @@ GEM multi_json (1.15.0) mutex_m (0.2.0) net-http-persistent (2.9.4) - net-imap (0.4.9.1) + net-imap (0.4.10) date net-protocol net-pop (0.1.2) @@ -245,14 +245,14 @@ GEM net-protocol net-ssh (7.2.1) netrc (0.11.0) - newrelic_rpm (9.7.0) + newrelic_rpm (9.7.1) oj (3.16.1) omni_logger (0.1.4) logger os (1.1.4) parallel (1.24.0) parseconfig (1.1.2) - parser (3.3.0.3) + parser (3.3.0.5) ast (~> 2.4.1) racc pony (1.13.1) @@ -284,7 +284,7 @@ GEM rdf (1.0.8) addressable (>= 2.2) redcarpet (3.6.0) - redis (5.0.8) + redis (5.1.0) redis-client (>= 0.17.0) redis-client (0.19.1) connection_pool @@ -303,11 +303,11 @@ GEM rsolr (2.5.0) builder (>= 2.1.2) faraday (>= 0.9, < 3, != 2.0.0) - rubocop (1.59.0) + rubocop (1.60.2) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.2.4) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) @@ -427,4 +427,4 @@ DEPENDENCIES unicorn-worker-killer BUNDLED WITH - 2.4.22 + 2.4.22 \ No newline at end of file diff --git a/controllers/users_controller.rb b/controllers/users_controller.rb index 1a02190f..fdad6a74 100644 --- a/controllers/users_controller.rb +++ b/controllers/users_controller.rb @@ -13,7 +13,7 @@ class UsersController < ApplicationController end ## - # This endpoint will create a token and store it on the user + # This endpoint will create a token and store it on the use- # An email is generated with this token, which allows the user # to click and login to the UI. The token can then be provided to # the /reset_password endpoint to actually reset the password. @@ -24,6 +24,7 @@ class UsersController < ApplicationController error 404, "User not found" unless user reset_token = token(36) user.resetToken = reset_token + user.resetTokenExpireTime = Time.now.to_i + 1.hours.to_i if user.valid? user.save(override_security: true) LinkedData::Utils::Notifications.reset_password(user, reset_token) @@ -46,10 +47,15 @@ class UsersController < ApplicationController user = LinkedData::Models::User.where(email: email, username: username).include(User.goo_attrs_to_load(includes_param)).first error 404, "User not found" unless user if token.eql?(user.resetToken) + error 401, "Invalid password reset token" if user.resetTokenExpireTime.nil? + error 401, "The password reset token expired" if user.resetTokenExpireTime < Time.now.to_i + user.resetToken = nil + user.resetTokenExpireTime = nil + user.save(override_security: true) if user.valid? user.show_apikey = true reply user else - error 403, "Password reset not authorized with this token" + error 401, "Password reset not authorized with this token" end end diff --git a/docker-compose.yml b/docker-compose.yml index 2e552b91..61119bb6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -65,9 +65,9 @@ services: GOO_BACKEND_NAME: ag GOO_PORT: 10035 GOO_HOST: agraph-ut - GOO_PATH_QUERY: /repositories/bioportal_test - GOO_PATH_DATA: /repositories/bioportal_test/statements - GOO_PATH_UPDATE: /repositories/bioportal_test/statements + GOO_PATH_QUERY: /repositories/ontoportal_test + GOO_PATH_DATA: /repositories/ontoportal_test/statements + GOO_PATH_UPDATE: /repositories/ontoportal_test/statements profiles: - agraph depends_on: @@ -114,7 +114,7 @@ services: retries: 5 agraph-ut: - image: franzinc/agraph:v8.0.0 + image: franzinc/agraph:v8.0.1 platform: linux/amd64 environment: - AGRAPH_SUPER_USER=test @@ -124,18 +124,19 @@ services: # - 10035:10035 command: > bash -c "/agraph/bin/agraph-control --config /agraph/etc/agraph.cfg start - ; agtool repos create bioportal_test + ; agtool repos create ontoportal_test ; agtool users add anonymous - ; agtool users grant anonymous root:bioportal_test:rw + ; agtool users grant anonymous root:ontoportal_test:rw ; tail -f /agraph/data/agraph.log" healthcheck: - test: ["CMD-SHELL", "agtool storage-report bioportal_test || exit 1"] - start_period: 10s - interval: 60s - timeout: 5s - retries: 3 + test: ["CMD-SHELL", "agtool storage-report ontoportal_test || exit 1"] + start_period: 30s + interval: 20s + timeout: 10s + retries: 10 profiles: - agraph volumes: bundle: + diff --git a/test/controllers/test_ontologies_controller.rb b/test/controllers/test_ontologies_controller.rb index da8c6c11..6a4c9a27 100644 --- a/test/controllers/test_ontologies_controller.rb +++ b/test/controllers/test_ontologies_controller.rb @@ -287,6 +287,8 @@ def test_on_demand_ontology_pull post "/ontologies/#{acronym}/pull?apikey=#{blocked_user.apikey}" assert_equal(403, last_response.status, msg="An unauthorized user was able to execute the on-demand pull") ensure + del = User.find("blocked").first + del.delete if del stop_server LinkedData.settings.enable_security = false end diff --git a/test/controllers/test_users_controller.rb b/test/controllers/test_users_controller.rb index 6064132e..53bf520f 100644 --- a/test/controllers/test_users_controller.rb +++ b/test/controllers/test_users_controller.rb @@ -67,9 +67,58 @@ def test_create_new_user get "/users/#{@@username}" assert last_response.ok? - assert MultiJson.load(last_response.body)["username"].eql?(@@username) + assert MultiJson.load(last_response.body)["username"].eql?(@@username) + assert_equal "test_user@example.org", MultiJson.load(last_response.body)["email"] + end + + def test_reset_password + username = 'resetpswd' + user = {email: "#{username}@example.org", password: "resetme"} + put "/users/#{username}", MultiJson.dump(user), "CONTENT_TYPE" => "application/json" + assert last_response.status == 201 + user = User.find(username).include(User.attributes).first + assert_nil user.resetToken + post "/users/create_reset_password_token", {username: username, email: "bademail@example.org"} + user = User.find(username).include(User.attributes).first + assert_nil user.resetToken + assert_equal 404, last_response.status + post "/users/reset_password", {username: 'badusername', email: "#{username}@example.org", token: 'badtoken'} + post "/users/create_reset_password_token", {username: username, email: "#{username}@example.org"} + assert_equal 204, last_response.status + user = User.find(username).include(User.attributes).first + refute_nil user.resetToken + post "/users/reset_password", {username: username, email: "#{username}@example.org", token: 'badtoken'} + assert_equal 401, last_response.status + post "/users/reset_password", {username: 'badusername', email: "#{username}@example.org", token: 'badtoken'} + assert_equal 404, last_response.status + post "/users/reset_password", {username: username, token: user.resetToken} + assert_equal 404, last_response.status + post "/users/reset_password", {email: "#{username}@example.org", token: user.resetToken} + assert_equal 404, last_response.status + post "/users/reset_password", {username: username, email: "badexampe@example.org", token: user.resetToken} + assert_equal 404, last_response.status + post "/users/reset_password", {username: username, email: "#{username}@example.org", token: user.resetToken} + assert_equal 200, last_response.status + assert_equal "#{username}@example.org", MultiJson.load(last_response.body)["email"] + user = User.find(username).include(User.attributes).first + assert_nil user.resetToken end + def test_reset_password_expired_token + username = 'resetexpired' + user = {email: "#{username}@example.org", password: "resetme"} + put "/users/#{username}", MultiJson.dump(user), "CONTENT_TYPE" => "application/json" + assert last_response.status == 201 + post "/users/create_reset_password_token", {username: username, email: "#{username}@example.org"} + assert_equal 204, last_response.status + user = User.find(username).include(User.attributes).first + user.resetTokenExpireTime = Time.now.to_i - 2.hours.to_i + user.save + post "/users/reset_password", {username: username, email: "#{username}@example.org", token: user.resetToken} + assert_equal 401, last_response.status + end + + def test_create_new_invalid_user put "/users/totally_new_user" assert last_response.status == 422 @@ -120,7 +169,6 @@ def test_authentication assert user["username"].eql?(@@usernames.first) end - private def _delete_user(username)