mirror of
https://github.com/czhu12/canine.git
synced 2025-12-16 16:35:10 -06:00
update form update fields
This commit is contained in:
2
Gemfile
2
Gemfile
@@ -116,3 +116,5 @@ gem 'flipper-active_record', '~> 1.2.2'
|
||||
gem 'flipper-ui', '~> 1.2.2'
|
||||
|
||||
gem "net-ldap", "~> 0.20.0"
|
||||
|
||||
gem "jwt", "~> 2.9"
|
||||
|
||||
@@ -723,6 +723,7 @@ DEPENDENCIES
|
||||
importmap-rails
|
||||
jbuilder
|
||||
jsbundling-rails
|
||||
jwt (~> 2.9)
|
||||
k8s-ruby (~> 0.17.2)
|
||||
kubeclient (~> 4.12)
|
||||
light-service (~> 0.20.0)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
class SSO::SyncUserTeams
|
||||
extend LightService::Organizer
|
||||
def self.call(email:, team_names:, account:, sso_provider:, uid:, name: nil)
|
||||
with(email:, team_names:, account:, sso_provider:, uid:, name:).reduce(
|
||||
SSO::CreateTeamsInAccount,
|
||||
SSO::CreateUserInAccount,
|
||||
SSO::SyncTeams,
|
||||
)
|
||||
def self.call(email:, team_names:, account:, sso_provider:, uid:, name: nil, create_teams: false)
|
||||
actions = []
|
||||
actions << SSO::CreateTeamsInAccount if create_teams
|
||||
actions << SSO::CreateUserInAccount
|
||||
actions << SSO::SyncTeams if create_teams
|
||||
|
||||
with(email:, team_names:, account:, sso_provider:, uid:, name:).reduce(actions)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,8 +4,8 @@ module Accounts
|
||||
before_action :load_account
|
||||
|
||||
def authorize
|
||||
oidc_config = @account.sso_provider&.configuration
|
||||
unless oidc_config.is_a?(OIDCConfiguration)
|
||||
oidc_configuration = @account.sso_provider&.configuration
|
||||
unless oidc_configuration.is_a?(OIDCConfiguration)
|
||||
redirect_to account_sign_in_path(@account.slug), alert: "OIDC is not configured for this account"
|
||||
return
|
||||
end
|
||||
@@ -15,8 +15,8 @@ module Accounts
|
||||
session[:oidc_state] = state
|
||||
|
||||
# Build authorization URL
|
||||
auth_url = build_authorization_url(oidc_config, state)
|
||||
redirect_to auth_url, allow_other_host: true
|
||||
authorization_url = build_authorization_url(oidc_configuration, state)
|
||||
redirect_to authorization_url, allow_other_host: true
|
||||
end
|
||||
|
||||
def callback
|
||||
@@ -26,9 +26,9 @@ module Accounts
|
||||
return
|
||||
end
|
||||
|
||||
oidc_config = @account.sso_provider&.configuration
|
||||
oidc_configuration = @account.sso_provider&.configuration
|
||||
|
||||
unless oidc_config.is_a?(OIDCConfiguration)
|
||||
unless oidc_configuration.is_a?(OIDCConfiguration)
|
||||
redirect_to root_path, alert: "OIDC is not configured"
|
||||
return
|
||||
end
|
||||
@@ -39,7 +39,7 @@ module Accounts
|
||||
end
|
||||
|
||||
# Exchange code for tokens
|
||||
result = OIDC::Authenticator.new(oidc_config).authenticate(
|
||||
result = OIDC::Authenticator.new(oidc_configuration).authenticate(
|
||||
code: params[:code],
|
||||
redirect_uri: oidc_callback_url(slug: @account.slug)
|
||||
)
|
||||
@@ -51,26 +51,15 @@ module Accounts
|
||||
|
||||
# Create or find user
|
||||
sso_provider = @account.sso_provider
|
||||
if sso_provider.just_in_time_team_provisioning_mode?
|
||||
ar_result = ActiveRecord::Base.transaction do
|
||||
SSO::SyncUserTeams.call(
|
||||
email: result.email,
|
||||
team_names: result.groups || [],
|
||||
account: @account,
|
||||
sso_provider: sso_provider,
|
||||
uid: result.uid,
|
||||
name: result.name
|
||||
)
|
||||
end
|
||||
else
|
||||
ar_result = SSO::CreateUserInAccount.execute(
|
||||
email: result.email,
|
||||
account: @account,
|
||||
sso_provider: sso_provider,
|
||||
uid: result.uid,
|
||||
name: result.name
|
||||
)
|
||||
end
|
||||
ar_result = SSO::SyncUserTeams.call(
|
||||
email: result.email,
|
||||
team_names: result.groups || [],
|
||||
account: @account,
|
||||
sso_provider: sso_provider,
|
||||
uid: result.uid,
|
||||
name: result.name,
|
||||
create_teams: sso_provider.just_in_time_team_provisioning_mode?
|
||||
)
|
||||
|
||||
if ar_result.failure?
|
||||
redirect_to account_sign_in_path(@account.slug), alert: "Failed to create user account"
|
||||
@@ -94,22 +83,22 @@ module Accounts
|
||||
redirect_to root_path, alert: "Account not found"
|
||||
end
|
||||
|
||||
def build_authorization_url(oidc_config, state)
|
||||
def build_authorization_url(oidc_configuration, state)
|
||||
params = {
|
||||
response_type: "code",
|
||||
client_id: oidc_config.client_id,
|
||||
client_id: oidc_configuration.client_id,
|
||||
redirect_uri: oidc_callback_url(slug: @account.slug),
|
||||
scope: oidc_config.scopes,
|
||||
scope: oidc_configuration.scopes,
|
||||
state: state
|
||||
}
|
||||
|
||||
auth_endpoint = oidc_config.authorization_endpoint.presence || discover_authorization_endpoint(oidc_config)
|
||||
auth_endpoint = oidc_configuration.authorization_endpoint.presence || discover_authorization_endpoint(oidc_configuration)
|
||||
"#{auth_endpoint}?#{params.to_query}"
|
||||
end
|
||||
|
||||
def discover_authorization_endpoint(oidc_config)
|
||||
def discover_authorization_endpoint(oidc_configuration)
|
||||
# Fetch from OIDC discovery document
|
||||
discovery_url = oidc_config.discovery_url
|
||||
discovery_url = oidc_configuration.discovery_url
|
||||
response = HTTP.get(discovery_url)
|
||||
if response.status.success?
|
||||
JSON.parse(response.body.to_s)["authorization_endpoint"]
|
||||
|
||||
@@ -27,7 +27,7 @@ class OIDCConfiguration < ApplicationRecord
|
||||
validates :uid_claim, presence: true
|
||||
|
||||
def discovery_url
|
||||
"#{issuer.chomp('/')}/.well-known/openid-configuration"
|
||||
URI.join(issuer, ".well-known/openid-configuration").to_s
|
||||
end
|
||||
|
||||
def uses_discovery?
|
||||
|
||||
@@ -41,4 +41,8 @@ class SSOProvider < ApplicationRecord
|
||||
def oidc?
|
||||
configuration_type == "OIDCConfiguration"
|
||||
end
|
||||
|
||||
def sso_users_count
|
||||
providers.joins(:user).distinct.count(:user_id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -86,8 +86,6 @@ module OIDC
|
||||
|
||||
def extract_claims(id_token, access_token)
|
||||
if id_token.present?
|
||||
# Decode JWT without verification for now (verification should be added for production)
|
||||
# The ID token contains the user claims
|
||||
payload = decode_jwt(id_token)
|
||||
return payload if payload.is_a?(Result)
|
||||
payload
|
||||
@@ -100,14 +98,32 @@ module OIDC
|
||||
end
|
||||
|
||||
def decode_jwt(token)
|
||||
# Simple JWT decode (without signature verification - should add JWKS verification for production)
|
||||
parts = token.split(".")
|
||||
return Result.new(success?: false, error_message: "Invalid JWT format") if parts.length < 2
|
||||
jwks = fetch_jwks
|
||||
decoded = JWT.decode(token, nil, true, {
|
||||
algorithms: %w[RS256 RS384 RS512 ES256 ES384 ES512],
|
||||
jwks: jwks,
|
||||
iss: config.issuer,
|
||||
aud: config.client_id,
|
||||
verify_iss: true,
|
||||
verify_aud: true
|
||||
})
|
||||
decoded.first
|
||||
rescue JWT::DecodeError => e
|
||||
@logger.error "OIDC auth: JWT decode error - #{e.message}"
|
||||
Result.new(success?: false, error_message: "Invalid token: #{e.message}")
|
||||
end
|
||||
|
||||
payload = Base64.urlsafe_decode64(parts[1] + "=" * (4 - parts[1].length % 4))
|
||||
JSON.parse(payload)
|
||||
rescue => e
|
||||
Result.new(success?: false, error_message: "Failed to decode JWT: #{e.message}")
|
||||
def fetch_jwks
|
||||
return @jwks_cache if @jwks_cache
|
||||
|
||||
jwks_uri = config.jwks_uri.presence || discover_endpoint("jwks_uri")
|
||||
response = HTTP.get(jwks_uri)
|
||||
|
||||
unless response.status.success?
|
||||
raise JWT::DecodeError, "Failed to fetch JWKS from #{jwks_uri}"
|
||||
end
|
||||
|
||||
@jwks_cache = JWT::JWK::Set.new(JSON.parse(response.body.to_s))
|
||||
end
|
||||
|
||||
def fetch_userinfo(access_token)
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
label: "Bind Password",
|
||||
description: "Password for the bind DN"
|
||||
)) do %>
|
||||
<%= ldap_form.password_field :bind_password, class: "input input-bordered w-full" %>
|
||||
<%= ldap_form.text_field :bind_password, type: "password", class: "input input-bordered w-full" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
label: "Client Secret",
|
||||
description: "OAuth 2.0 client secret"
|
||||
)) do %>
|
||||
<%= oidc_form.password_field :client_secret, class: "input input-bordered w-full", required: true %>
|
||||
<%= oidc_form.text_field :client_secret, type: "password", class: "input input-bordered w-full", required: true %>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">* Required</span>
|
||||
</label>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<div class="alert mb-4">
|
||||
<div class="alert alert-warning mb-4">
|
||||
<iconify-icon icon="lucide:info" class="mr-2"></iconify-icon>
|
||||
<div>
|
||||
<div class="font-medium">Redirect URI</div>
|
||||
<div class="text-sm mt-1">Configure this redirect URI in your OIDC provider:</div>
|
||||
<code class="block mt-2 text-xs bg-base-300 p-2 rounded break-all select-all"><%= oidc_callback_url(slug: current_account.slug) %></code>
|
||||
<pre
|
||||
class="inline-block cursor-pointer mt-2"
|
||||
data-controller="clipboard"
|
||||
data-clipboard-text="<%= oidc_callback_url(slug: current_account.slug) %>"><%= oidc_callback_url(slug: current_account.slug) %></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<% else %>
|
||||
<span class="badge badge-ghost">Disabled</span>
|
||||
<% end %>
|
||||
<span class="badge badge-info"><%= @sso_provider.sso_users_count %> <%= "user".pluralize(@sso_provider.sso_users_count) %></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
|
||||
@@ -51,27 +51,15 @@ module Devise
|
||||
|
||||
email = result.email
|
||||
|
||||
if sso_provider.just_in_time_team_provisioning_mode?
|
||||
groups = result.groups
|
||||
ar_result = ActiveRecord::Base.transaction do
|
||||
SSO::SyncUserTeams.call(
|
||||
email: email,
|
||||
team_names: groups,
|
||||
account: ldap_configuration.account,
|
||||
sso_provider: sso_provider,
|
||||
uid: result.user_dn,
|
||||
name: result.name
|
||||
)
|
||||
end
|
||||
else
|
||||
ar_result = SSO::CreateUserInAccount.execute(
|
||||
email: email,
|
||||
account: ldap_configuration.account,
|
||||
sso_provider: sso_provider,
|
||||
uid: result.user_dn,
|
||||
name: result.name
|
||||
)
|
||||
end
|
||||
ar_result = SSO::SyncUserTeams.call(
|
||||
email: email,
|
||||
team_names: result.groups || [],
|
||||
account: ldap_configuration.account,
|
||||
sso_provider: sso_provider,
|
||||
uid: result.user_dn,
|
||||
name: result.name,
|
||||
create_teams: sso_provider.just_in_time_team_provisioning_mode?
|
||||
)
|
||||
|
||||
if ar_result.failure?
|
||||
return fail(:invalid_login)
|
||||
|
||||
@@ -13,7 +13,8 @@ RSpec.describe SSO::SyncUserTeams do
|
||||
team_names: [ { name: 'Existing' }, { name: 'NewTeam' } ],
|
||||
account: account,
|
||||
sso_provider: sso_provider,
|
||||
uid: 'user-uid-123'
|
||||
uid: 'user-uid-123',
|
||||
create_teams: true
|
||||
)
|
||||
|
||||
expect(result).to be_success
|
||||
@@ -36,7 +37,8 @@ RSpec.describe SSO::SyncUserTeams do
|
||||
team_names: [ { name: 'KeepTeam' } ],
|
||||
account: account,
|
||||
sso_provider: sso_provider,
|
||||
uid: 'existing-uid'
|
||||
uid: 'existing-uid',
|
||||
create_teams: true
|
||||
)
|
||||
|
||||
expect(result).to be_success
|
||||
@@ -58,7 +60,8 @@ RSpec.describe SSO::SyncUserTeams do
|
||||
team_names: [],
|
||||
account: account,
|
||||
sso_provider: sso_provider,
|
||||
uid: 'existing-uid'
|
||||
uid: 'existing-uid',
|
||||
create_teams: true
|
||||
)
|
||||
|
||||
expect(result).to be_success
|
||||
|
||||
@@ -24,5 +24,28 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SSOProvider, type: :model do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
describe '#sso_users_count' do
|
||||
let(:sso_provider) { create(:sso_provider) }
|
||||
|
||||
it 'returns 0 when no users have signed in' do
|
||||
expect(sso_provider.sso_users_count).to eq(0)
|
||||
end
|
||||
|
||||
it 'counts users who have signed in via SSO' do
|
||||
user1 = create(:user)
|
||||
user2 = create(:user)
|
||||
create(:provider, user: user1, sso_provider: sso_provider, uid: 'uid-1', provider: sso_provider.name)
|
||||
create(:provider, user: user2, sso_provider: sso_provider, uid: 'uid-2', provider: sso_provider.name)
|
||||
|
||||
expect(sso_provider.sso_users_count).to eq(2)
|
||||
end
|
||||
|
||||
it 'does not count users from other SSO providers' do
|
||||
other_sso_provider = create(:sso_provider, account: create(:account))
|
||||
user = create(:user)
|
||||
create(:provider, user: user, sso_provider: other_sso_provider, uid: 'uid-1', provider: other_sso_provider.name)
|
||||
|
||||
expect(sso_provider.sso_users_count).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user