Commit 380ee132 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch '9288-create-a-scim-specific-token-for-group-sso-be' into 'master'

Resolve "Create a SCIM specific token for group SSO [BE]"

Closes #9288

See merge request gitlab-org/gitlab-ee!9786
parents 92ec08df 25deeaa3
......@@ -2773,6 +2773,14 @@ ActiveRecord::Schema.define(version: 20190305162221) do
t.index ["group_id"], name: "index_saml_providers_on_group_id", using: :btree
end
create_table "scim_oauth_access_tokens", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "group_id", null: false
t.string "token_encrypted", null: false
t.index ["group_id", "token_encrypted"], name: "index_scim_oauth_access_tokens_on_group_id_and_token_encrypted", unique: true, using: :btree
end
create_table "sent_notifications", force: :cascade do |t|
t.integer "project_id"
t.integer "noteable_id"
......@@ -3613,6 +3621,7 @@ ActiveRecord::Schema.define(version: 20190305162221) do
add_foreign_key "reviews", "projects", on_delete: :cascade
add_foreign_key "reviews", "users", column: "author_id", on_delete: :nullify
add_foreign_key "saml_providers", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "scim_oauth_access_tokens", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade
add_foreign_key "slack_integrations", "services", on_delete: :cascade
add_foreign_key "smartcard_identities", "users", on_delete: :cascade
......
# frozen_string_literal: true
module SamlAuthorization
extend ActiveSupport::Concern
private
def authorize_manage_saml!
render_404 unless can?(current_user, :admin_group_saml, group)
end
def check_group_saml_configured
render_404 unless Gitlab::Auth::GroupSaml::Config.enabled?
end
def require_top_level_group
render_404 if group.subgroup?
end
end
# frozen_string_literal: true
require_relative '../concerns/saml_authorization.rb'
class Groups::SamlProvidersController < Groups::ApplicationController
include SamlAuthorization
before_action :require_top_level_group
before_action :authorize_manage_saml!
before_action :check_group_saml_available!
......@@ -8,6 +10,10 @@ class Groups::SamlProvidersController < Groups::ApplicationController
def show
@saml_provider = @group.saml_provider || @group.build_saml_provider
scim_token = ScimOauthAccessToken.find_by_group_id(@group.id)
@scim_token_url = scim_token.as_entity_json[:scim_api_url] if scim_token
end
def create
......@@ -28,18 +34,6 @@ class Groups::SamlProvidersController < Groups::ApplicationController
private
def authorize_manage_saml!
render_404 unless can?(current_user, :admin_group_saml, @group)
end
def check_group_saml_configured
render_404 unless Gitlab::Auth::GroupSaml::Config.enabled?
end
def require_top_level_group
render_404 if @group.subgroup?
end
def saml_provider_params
allowed_params = %i[sso_url certificate_fingerprint enabled]
......
# frozen_string_literal: true
class Groups::ScimOauthController < Groups::ApplicationController
include SamlAuthorization
before_action :require_top_level_group
before_action :authorize_manage_saml!
before_action :check_group_saml_available!
before_action :check_group_saml_configured
before_action :check_group_scim_enabled
def show
scim_token = ScimOauthAccessToken.find_by_group_id(@group.id)
respond_to do |format|
format.json do
if scim_token
render json: scim_token.as_entity_json
else
render json: {}
end
end
end
end
# rubocop: disable CodeReuse/ActiveRecord
def create
scim_token = ScimOauthAccessToken.find_or_initialize_by(group: @group)
if scim_token.new_record?
scim_token.save
else
scim_token.reset_token!
end
respond_to do |format|
format.json do
if scim_token.valid?
render json: scim_token.as_entity_json
else
render json: { errors: scim_token.errors.full_messages }, status: :unprocessable_entity
end
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
private
def check_group_scim_enabled
route_not_found unless Feature.enabled?(:group_scim, @group)
end
end
......@@ -20,6 +20,7 @@ module EE
has_one :saml_provider
has_one :insight, foreign_key: :namespace_id
accepts_nested_attributes_for :insight
has_one :scim_oauth_access_token
has_many :ldap_group_links, foreign_key: 'group_id', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :hooks, dependent: :destroy, class_name: 'GroupHook' # rubocop:disable Cop/ActiveRecordDependent
......
# frozen_string_literal: true
class ScimOauthAccessToken < ApplicationRecord
include TokenAuthenticatable
belongs_to :group
add_authentication_token_field :token, encrypted: :required
validates :group, presence: true
before_save :ensure_token
def as_entity_json
ScimOauthAccessTokenEntity.new(self).as_json
end
end
# frozen_string_literal: true
class ScimOauthAccessTokenEntity < Grape::Entity
include ::API::Helpers::RelatedResourcesHelpers
SCIM_PATH = '/api/scim/v2/groups'
expose :scim_api_url do |scim|
expose_url("#{SCIM_PATH}/#{scim.group.full_path}")
end
expose :token, as: :scim_token
end
......@@ -89,6 +89,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
delete :unlink, to: 'sso#unlink'
end
resource :scim_oauth, only: [:show, :create], controller: :scim_oauth
resource :roadmap, only: [:show], controller: 'roadmap'
legacy_ee_group_boards_redirect = redirect do |params, request|
......
# frozen_string_literal: true
class CreateScimOauthAccessTokens < ActiveRecord::Migration[5.0]
DOWNTIME = false
def change
create_table :scim_oauth_access_tokens do |t|
t.timestamps_with_timezone null: false
t.references :group, null: false, index: false
t.string :token_encrypted, null: false
t.index [:group_id, :token_encrypted], unique: true
t.foreign_key :namespaces, column: :group_id, on_delete: :cascade
end
end
end
......@@ -79,6 +79,23 @@ describe Groups::SamlProvidersController do
expect(response).to render_template 'groups/saml_providers/show'
end
it 'has no SCIM token URL' do
group.add_owner(user)
subject
expect(assigns(:scim_token_url)).to be_nil
end
it 'has the SCIM token URL when it exists' do
create(:scim_oauth_access_token, group: group)
group.add_owner(user)
subject
expect(assigns(:scim_token_url)).to eq("http://localhost/api/scim/v2/groups/#{group.full_path}")
end
context 'not on a top level group', :nested_groups do
let(:group) { create(:group, :nested) }
......
# frozen_string_literal: true
require 'spec_helper'
describe Groups::ScimOauthController do
let(:saml_provider) { create(:saml_provider, group: group) }
let(:group) { create(:group, :private, parent_id: nil) }
let(:user) { create(:user) }
before do
sign_in(user)
end
def stub_saml_config(enabled:)
providers = enabled ? %i(group_saml) : []
allow(Devise).to receive(:omniauth_providers).and_return(providers)
end
context 'when the feature is enabled' do
before do
stub_saml_config(enabled: true)
stub_licensed_features(group_saml: true)
stub_feature_flags(group_scim: true)
end
describe 'GET #show' do
subject { get :show, params: { group_id: group }, format: :json }
before do
group.add_owner(user)
end
context 'without token' do
it 'shows an empty response' do
subject
expect(json_response).to eq({})
end
end
context 'with token' do
let!(:scim_token) { create(:scim_oauth_access_token, group: group) }
it 'shows the token' do
subject
expect(json_response['scim_token']).to eq(scim_token.token)
end
it 'shows the url' do
subject
expect(json_response['scim_api_url']).not_to be_empty
end
end
end
describe 'POST #create' do
subject { post :create, params: { group_id: group }, format: :json }
before do
group.add_owner(user)
end
context 'without token' do
it 'creates a new SCIM token record' do
expect { subject }.to change { ScimOauthAccessToken.count }.by(1)
end
context 'json' do
before do
subject
end
it 'shows the token' do
expect(json_response['scim_token']).not_to be_empty
end
it 'shows the url' do
expect(json_response['scim_api_url']).not_to be_empty
end
end
end
context 'with token' do
let!(:scim_token) { create(:scim_oauth_access_token, group: group) }
it 'does not create a new SCIM token record' do
expect { subject }.not_to change { ScimOauthAccessToken.count }
end
it 'updates the token' do
expect { subject }.to change { scim_token.reload.token }
end
context 'json' do
before do
subject
end
it 'shows the token' do
expect(json_response['scim_token']).to eq(scim_token.reload.token)
end
it 'shows the url' do
expect(json_response['scim_api_url']).not_to be_empty
end
end
end
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :scim_oauth_access_token do
group
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ScimOauthAccessToken do
describe "Associations" do
it { is_expected.to belong_to :group }
end
describe 'Validations' do
it { is_expected.to validate_presence_of(:group) }
end
describe '#token' do
it 'generates a token on creation' do
scim_token = described_class.create(group: create(:group))
expect(scim_token.token).to be_a(String)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ScimOauthAccessTokenEntity do
let(:entity) do
described_class.new(create(:scim_oauth_access_token))
end
subject { entity.as_json }
it "exposes the URL" do
is_expected.to include(:scim_api_url)
end
it "exposes the token" do
is_expected.to include(:scim_token)
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment