Skip to content

Commit 16c42de

Browse files
Merge pull request opf#18436 from opf/jwt-client-credentials
JWT: Allow authentication through client credentials
2 parents fbcd738 + da9aa9b commit 16c42de

File tree

7 files changed

+220
-2
lines changed

7 files changed

+220
-2
lines changed

Diff for: app/models/service_account.rb

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# frozen_string_literal: true
2+
3+
#-- copyright
4+
# OpenProject is an open source project management software.
5+
# Copyright (C) the OpenProject GmbH
6+
#
7+
# This program is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU General Public License version 3.
9+
#
10+
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
11+
# Copyright (C) 2006-2013 Jean-Philippe Lang
12+
# Copyright (C) 2010-2013 the ChiliProject Team
13+
#
14+
# This program is free software; you can redistribute it and/or
15+
# modify it under the terms of the GNU General Public License
16+
# as published by the Free Software Foundation; either version 2
17+
# of the License, or (at your option) any later version.
18+
#
19+
# This program is distributed in the hope that it will be useful,
20+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22+
# GNU General Public License for more details.
23+
#
24+
# You should have received a copy of the GNU General Public License
25+
# along with this program; if not, write to the Free Software
26+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27+
#
28+
# See COPYRIGHT and LICENSE files for more details.
29+
#++
30+
31+
class ServiceAccount < User
32+
alias_attribute(:name, :lastname)
33+
validates :name, presence: true
34+
validates :name, length: { maximum: 256 }
35+
36+
has_one :service_account_association, dependent: :destroy
37+
38+
def to_s
39+
name
40+
end
41+
42+
def available_custom_fields = []
43+
44+
def logged? = false
45+
46+
def builtin? = true
47+
48+
def mail = ""
49+
50+
def time_zone
51+
ActiveSupport::TimeZone[Setting.user_default_timezone.presence || "Etc/UTC"]
52+
end
53+
54+
def rss_key = nil
55+
end

Diff for: app/models/service_account_association.rb

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# frozen_string_literal: true
2+
3+
#-- copyright
4+
# OpenProject is an open source project management software.
5+
# Copyright (C) the OpenProject GmbH
6+
#
7+
# This program is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU General Public License version 3.
9+
#
10+
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
11+
# Copyright (C) 2006-2013 Jean-Philippe Lang
12+
# Copyright (C) 2010-2013 the ChiliProject Team
13+
#
14+
# This program is free software; you can redistribute it and/or
15+
# modify it under the terms of the GNU General Public License
16+
# as published by the Free Software Foundation; either version 2
17+
# of the License, or (at your option) any later version.
18+
#
19+
# This program is distributed in the hope that it will be useful,
20+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22+
# GNU General Public License for more details.
23+
#
24+
# You should have received a copy of the GNU General Public License
25+
# along with this program; if not, write to the Free Software
26+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27+
#
28+
# See COPYRIGHT and LICENSE files for more details.
29+
#++
30+
31+
class ServiceAccountAssociation < ApplicationRecord
32+
belongs_to :service_account
33+
belongs_to :service, polymorphic: true
34+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
#-- copyright
4+
# OpenProject is an open source project management software.
5+
# Copyright (C) the OpenProject GmbH
6+
#
7+
# This program is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU General Public License version 3.
9+
#
10+
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
11+
# Copyright (C) 2006-2013 Jean-Philippe Lang
12+
# Copyright (C) 2010-2013 the ChiliProject Team
13+
#
14+
# This program is free software; you can redistribute it and/or
15+
# modify it under the terms of the GNU General Public License
16+
# as published by the Free Software Foundation; either version 2
17+
# of the License, or (at your option) any later version.
18+
#
19+
# This program is distributed in the hope that it will be useful,
20+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22+
# GNU General Public License for more details.
23+
#
24+
# You should have received a copy of the GNU General Public License
25+
# along with this program; if not, write to the Free Software
26+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27+
#
28+
# See COPYRIGHT and LICENSE files for more details.
29+
#++
30+
31+
class CreateServiceAccountAssociations < ActiveRecord::Migration[7.1]
32+
def change
33+
create_table :service_account_associations do |t|
34+
t.belongs_to :service_account, null: false, index: { unique: true }
35+
t.belongs_to :service, null: false, index: false # necessary index covered by composite
36+
t.string :service_type, null: false
37+
38+
t.timestamps null: false
39+
40+
t.index %i[service_type service_id], unique: true
41+
end
42+
end
43+
end

Diff for: lib_static/open_project/authentication/strategies/warden/jwt_oidc.rb

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# frozen_string_literal: true
2+
13
module OpenProject
24
module Authentication
35
module Strategies
@@ -24,7 +26,11 @@ def authenticate!
2426
->(payload_and_provider) do
2527
payload, provider = payload_and_provider
2628
user = User.find_by(identity_url: "#{provider.slug}:#{payload['sub']}")
27-
success!(user) if user
29+
if user
30+
success!(user)
31+
else
32+
fail_with_header!(error: "invalid_token", error_description: "The user identified by the token is not known")
33+
end
2834
end,
2935
->(error) { fail_with_header!(error: "invalid_token", error_description: error) }
3036
)

Diff for: spec/factories/service_account_factory.rb

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
#-- copyright
4+
# OpenProject is an open source project management software.
5+
# Copyright (C) the OpenProject GmbH
6+
#
7+
# This program is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU General Public License version 3.
9+
#
10+
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
11+
# Copyright (C) 2006-2013 Jean-Philippe Lang
12+
# Copyright (C) 2010-2013 the ChiliProject Team
13+
#
14+
# This program is free software; you can redistribute it and/or
15+
# modify it under the terms of the GNU General Public License
16+
# as published by the Free Software Foundation; either version 2
17+
# of the License, or (at your option) any later version.
18+
#
19+
# This program is distributed in the hope that it will be useful,
20+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22+
# GNU General Public License for more details.
23+
#
24+
# You should have received a copy of the GNU General Public License
25+
# along with this program; if not, write to the Free Software
26+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27+
#
28+
# See COPYRIGHT and LICENSE files for more details.
29+
#++
30+
31+
FactoryBot.define do
32+
factory :service_account, parent: :user, class: "ServiceAccount" do
33+
transient do
34+
service { nil }
35+
end
36+
37+
after(:create) do |instance, evaluator|
38+
if evaluator.service.present?
39+
instance.create_service_account_association!(service: evaluator.service)
40+
end
41+
end
42+
end
43+
end

Diff for: spec/requests/api/v3/authentication_spec.rb

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# frozen_string_literal: true
2+
13
#-- copyright
24
# OpenProject is an open source project management software.
35
# Copyright (C) the OpenProject GmbH
@@ -421,10 +423,11 @@ def set_basic_auth_header(user, password)
421423
.to_return(status: 200, body: JWT::JWK::Set.new(jwk_response).export.to_json, headers: {})
422424
end
423425
let(:jwk_response) { jwk }
426+
let(:user) { create(:user, identity_url: "keycloak:#{token_sub}") }
424427

425428
before do
426429
create(:oidc_provider, slug: "keycloak")
427-
create(:user, identity_url: "keycloak:#{token_sub}")
430+
user.save!
428431
keys_request_stub
429432

430433
header "Authorization", "Bearer #{token}"
@@ -511,5 +514,18 @@ def set_basic_auth_header(user, password)
511514
.to eq(%{Bearer realm="OpenProject API", error="invalid_token", error_description="#{error}"})
512515
end
513516
end
517+
518+
context "when user identified by token is not known" do
519+
let(:user) { create(:user, identity_url: "keycloak:not-the-token-sub") }
520+
521+
it "fails with HTTP 401 Unauthorized" do
522+
get resource
523+
expect(last_response).to have_http_status :unauthorized
524+
expect(JSON.parse(last_response.body)).to eq(error_response_body)
525+
error = "The user identified by the token is not known"
526+
expect(last_response.header["WWW-Authenticate"])
527+
.to eq(%{Bearer realm="OpenProject API", error="invalid_token", error_description="#{error}"})
528+
end
529+
end
514530
end
515531
end

Diff for: spec/workers/principals/delete_job_integration_spec.rb

+21
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# frozen_string_literal: true
2+
13
#-- copyright
24
# OpenProject is an open source project management software.
35
# Copyright (C) the OpenProject GmbH
@@ -516,6 +518,25 @@
516518
end
517519
end
518520

521+
context "with a service account" do
522+
describe "service account association" do
523+
let(:principal) { create(:service_account, service:) }
524+
let(:service) { create(:oauth_application) }
525+
526+
before do
527+
principal.save!
528+
end
529+
530+
it "deletes the service account association" do
531+
expect { job }.to change(ServiceAccountAssociation, :count).from(1).to(0)
532+
end
533+
534+
it "does not delete the service associated to the service account" do
535+
expect { job }.not_to change(Doorkeeper::Application, :count)
536+
end
537+
end
538+
end
539+
519540
context "with a placeholder user" do
520541
let(:principal) { create(:placeholder_user) }
521542

0 commit comments

Comments
 (0)