mirror of
https://github.com/we-promise/sure.git
synced 2026-09-08 16:14:23 +00:00
* Add admin family management features and tests
- Implement FamiliesController with destroy action to delete unused families.
- Add localization for success and error messages related to family deletion.
- Create FamiliesControllerTest to ensure proper functionality of family deletion.
- Update UserPolicyTest to include permissions for super admins to delete users.
- Enhance UsersControllerTest with tests for user family management, including moving users between families and creating new families.
* feat(users): enhance user management with family transfer validation and improved delete warnings
* Simplify user management actions column and combine family options
Move heavy user edit forms from table rows into a DS::Popover action
menu, add role badges to the user column, combine family migration and
creation inputs with a Stimulus controller, enable self-family
transfer for super admins, and add safety guards against demoting the
last super admin in the system.
* feat: add authentication type pills to admin user index to display SSO and local login status
* Add set password feature for local users in admin user management
- Add password field in action popover for users with local password login
- Enforce all registration password criteria (min 8 chars, mixed case, digit, special char)
- Block simultaneous family and password updates with clear error
- Show descriptive success notifications (role, password, both, family)
- Ignore password param for SSO-only users
- Add comprehensive tests for all password validation paths
* Resolve DS Drift Patrol findings and CI scan failures
- Wrap auth-type pills in DS::Tooltip instead of native title= attribute
- Add actions.manage_user key to locale and drop redundant default: fallbacks
- Fix RuboCop style offenses in Admin::UsersController
- Update Brakeman ignore entry fingerprint for Admin::UsersController#user_params
* fix: update badge query to target DS::Pill structure
* Fix DS::Tooltip misuse hiding SSO auth-type pill in admin users view
The SSO pill was passed as a block to DS::Tooltip, which caused it to
render inside the hidden div[role="tooltip"] instead of being visible.
The text: option ("SSO Provider: ...") was also silently ignored because
tooltip_content returns content (the block) over @text when a block is
given.
Fix: render the SSO/Local+SSO pill directly as visible content and pass
DS::Tooltip with no block so text: is used as the tooltip popup. An info
icon now appears next to the pill and shows the provider name on hover.
Fixes test: Admin::UsersControllerTest#test_index_renders_auth_type_pills_for_local_and_sso_users
* Remove redundant default: fallback from role pill i18n lookup
All admin.users.index.roles.{guest,member,admin,super_admin} keys are
defined in the locale file and used elsewhere in the same view without
a default:. The fallback was redundant for every valid role and would
silently mask a missing or renamed key instead of raising in
development.
Drop the default: user.role.humanize argument so that any future
missing key surfaces immediately as I18n::MissingTranslationData.
* Revert unrelated JS/schema/split churn; fix transfer_to_family! default role
- Revert 62 JS files (Biome formatter and unrelated controller changes)
- Revert db/schema.rb dump churn (no new migrations in this branch)
- Revert unrelated split transaction view changes (edit/new.html.erb)
- Fix User#transfer_to_family! role default: role: role evaluates to nil
when omitted; use explicit self.role to read model attribute
Keeps the PR focused on user/family management (~18-20 files).
* Fix last login and session count in admin user management
Store last_login_at and sessions_count directly on the users table
so they remain accurate after a user logs out.
- Add migration to add last_login_at (datetime) and sessions_count
(integer, default 0) columns to users, with backfill from sessions
- Add counter_cache: :sessions_count to Session#belongs_to :user so
the count auto-increments/decrements on session create/destroy
- Add after_create callback on Session to stamp user.last_login_at
- Update Admin::UsersController to read both values from users table
instead of aggregating Session rows (which disappear on logout)
* Fix user management PR pending CI items
* Keep test current session after sign in
* Address PR review comments for user transfers
* refactor: update user removal label to "Delete User" and standardize component attribute naming
* Address PR Review Feedback for User Management
* test: Fix families and users controller tests for user management PR
* Limit PR 2868 schema diff
* Fix PR 2868 user management CI failures
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: sure-admin <sure-admin@splashblot.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
126 lines
4.3 KiB
Ruby
126 lines
4.3 KiB
Ruby
class OidcIdentity < ApplicationRecord
|
|
belongs_to :user
|
|
|
|
validates :provider, presence: true
|
|
validates :uid, presence: true, uniqueness: { scope: :provider }
|
|
validates :user_id, presence: true
|
|
|
|
# Update the last authenticated timestamp
|
|
def record_authentication!
|
|
update!(last_authenticated_at: Time.current)
|
|
end
|
|
|
|
# Sync user attributes from IdP on each login
|
|
# Updates stored identity info and syncs name to user (not email - that's identity)
|
|
def sync_user_attributes!(auth)
|
|
# Extract groups from claims (various common claim names)
|
|
groups = extract_groups(auth)
|
|
|
|
# Update stored identity info with latest from IdP
|
|
update!(info: {
|
|
email: auth.info&.email,
|
|
name: auth.info&.name,
|
|
first_name: auth.info&.first_name,
|
|
last_name: auth.info&.last_name,
|
|
groups: groups
|
|
})
|
|
|
|
# Sync name to user only when Sure has nothing on file (first link, or an
|
|
# admin blanked the field). Edits made inside Sure must survive subsequent
|
|
# SSO logins — previously the IdP value won unconditionally and clobbered
|
|
# any manually-edited name on every login (#1103).
|
|
user.update!(
|
|
first_name: user.first_name.presence || auth.info&.first_name.presence,
|
|
last_name: user.last_name.presence || auth.info&.last_name.presence
|
|
)
|
|
|
|
# Apply role mapping based on group membership
|
|
apply_role_mapping!(groups)
|
|
end
|
|
|
|
# Extract groups from various common IdP claim formats
|
|
def extract_groups(auth)
|
|
# Try various common group claim locations
|
|
groups = auth.extra&.raw_info&.groups ||
|
|
auth.extra&.raw_info&.[]("groups") ||
|
|
auth.extra&.raw_info&.[]("Group") ||
|
|
auth.info&.groups ||
|
|
auth.extra&.raw_info&.[]("http://schemas.microsoft.com/ws/2008/06/identity/claims/groups") ||
|
|
auth.extra&.raw_info&.[]("cognito:groups") ||
|
|
[]
|
|
|
|
# Normalize to array of strings
|
|
Array(groups).map(&:to_s)
|
|
end
|
|
|
|
# Apply role mapping based on IdP group membership
|
|
def apply_role_mapping!(groups)
|
|
config = provider_config
|
|
return unless config.present?
|
|
|
|
role_mapping = config.dig(:settings, :role_mapping) || config.dig(:settings, "role_mapping")
|
|
return unless role_mapping.present?
|
|
|
|
# Check roles in order of precedence (highest to lowest)
|
|
%w[super_admin admin member guest].each do |role|
|
|
mapped_groups = role_mapping[role] || role_mapping[role.to_sym] || []
|
|
mapped_groups = Array(mapped_groups)
|
|
|
|
# Check if user is in any of the mapped groups
|
|
if mapped_groups.include?("*") || (mapped_groups & groups).any?
|
|
# Only update if different to avoid unnecessary writes
|
|
user.update!(role: role) unless user.role == role
|
|
Rails.logger.info("[SSO] Applied role mapping: user_id=#{user.id} role=#{role} groups=#{groups}")
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
# Extract and store relevant info from OmniAuth auth hash
|
|
def self.create_from_omniauth(auth, user)
|
|
SsoIdentityBlock.with_identity_lock(provider: auth.provider, uid: auth.uid) do
|
|
if SsoIdentityBlock.blocked?(provider: auth.provider, uid: auth.uid)
|
|
raise SsoIdentityBlock::BlockedIdentity
|
|
end
|
|
|
|
# Extract issuer from OIDC auth response if available
|
|
issuer = auth.extra&.raw_info&.iss || auth.extra&.raw_info&.[]("iss")
|
|
|
|
create!(
|
|
user: user,
|
|
provider: auth.provider,
|
|
uid: auth.uid,
|
|
issuer: issuer,
|
|
info: {
|
|
email: auth.info&.email,
|
|
name: auth.info&.name,
|
|
first_name: auth.info&.first_name,
|
|
last_name: auth.info&.last_name
|
|
},
|
|
last_authenticated_at: Time.current
|
|
)
|
|
end
|
|
end
|
|
|
|
# Find the configured provider for this identity
|
|
def provider_config
|
|
AuthConfig.sso_providers&.find do |p|
|
|
p_name = p[:name] || p["name"]
|
|
p_id = p[:id] || p["id"]
|
|
p_name == provider || p_id == provider
|
|
end
|
|
end
|
|
|
|
# Validate that the stored issuer matches the configured provider's issuer
|
|
# Returns true if valid, false if mismatch (security concern)
|
|
def issuer_matches_config?
|
|
return true if issuer.blank? # Backward compatibility for old records
|
|
|
|
config = provider_config
|
|
config_issuer = config&.dig(:issuer) || config&.dig("issuer")
|
|
return true if config_issuer.blank? # No config to validate against
|
|
|
|
issuer == config_issuer
|
|
end
|
|
end
|