mirror of
https://github.com/System-End/identity-vault.git
synced 2026-04-19 22:05:07 +00:00
298 lines
10 KiB
Ruby
298 lines
10 KiB
Ruby
module SCIMService
|
|
class << self
|
|
SCIM_BASE_URL = "https://api.slack.com/scim/v2"
|
|
|
|
def find_or_create_user(identity:, scenario:)
|
|
if Rails.env.staging?
|
|
Rails.logger.info "Skipping Slack provisioning in staging for #{identity.primary_email}"
|
|
return {
|
|
success: true,
|
|
slack_id: "U_STAGING_#{identity.id}",
|
|
created: true,
|
|
message: "Staging mode: Slack integration disabled"
|
|
}
|
|
end
|
|
|
|
email = identity.primary_email
|
|
|
|
# Check if user exists - use Web API if not enterprise, SCIM API if enterprise
|
|
existing_slack_id = if Flipper.enabled?(:are_we_enterprise_yet, identity)
|
|
find_existing_user_by_email(email)
|
|
else
|
|
SlackService.find_by_email(email)
|
|
end
|
|
|
|
if existing_slack_id
|
|
Rails.logger.info "Slack user already exists for #{email}: #{existing_slack_id}"
|
|
|
|
workspace_status = SlackService.user_workspace_status(user_id: existing_slack_id)
|
|
needs_workspace_assignment = workspace_status != :in_workspace && workspace_status != :deactivated
|
|
|
|
if workspace_status == :deactivated
|
|
Rails.logger.info "Existing Slack user #{existing_slack_id} is deactivated, skipping workspace assignment"
|
|
elsif needs_workspace_assignment
|
|
Rails.logger.info "Existing Slack user #{existing_slack_id} is not in workspace (status: #{workspace_status}), will need assignment"
|
|
Sentry.capture_message(
|
|
"Existing Slack user found but not in workspace",
|
|
level: :info,
|
|
extra: {
|
|
identity_id: identity.id,
|
|
identity_public_id: identity.public_id,
|
|
identity_email: email,
|
|
slack_id: existing_slack_id,
|
|
workspace_status: workspace_status,
|
|
scenario: scenario.class.name,
|
|
scenario_slug: scenario.class.slug,
|
|
slack_user_type: scenario.slack_user_type,
|
|
slack_channels: scenario.slack_channels,
|
|
onboarding_scenario: identity.onboarding_scenario
|
|
}
|
|
)
|
|
end
|
|
|
|
return {
|
|
success: true,
|
|
slack_id: existing_slack_id,
|
|
created: false,
|
|
needs_workspace_assignment: needs_workspace_assignment,
|
|
message: "Linked existing Slack account"
|
|
}
|
|
end
|
|
|
|
unless Flipper.enabled?(:are_we_enterprise_yet, identity)
|
|
Rails.logger.info "SCIM user creation disabled (not enterprise yet) for #{email}"
|
|
return {
|
|
success: false,
|
|
error: "No existing Slack account found for #{email}",
|
|
created: false
|
|
}
|
|
end
|
|
|
|
Rails.logger.info "No existing Slack user found for #{email}, proceeding to create"
|
|
|
|
create_user(identity: identity, scenario: scenario)
|
|
end
|
|
|
|
def create_user(identity:, scenario:)
|
|
if Flipper.enabled?(:disable_slack_invites, identity)
|
|
Rails.logger.info "Slack invite creation disabled via Flipper for #{identity.primary_email}"
|
|
return {
|
|
success: false,
|
|
error: "Slack signups are temporarily paused...",
|
|
created: false
|
|
}
|
|
end
|
|
|
|
username = generate_unique_username(identity.primary_email)
|
|
user_type = scenario.slack_user_type
|
|
|
|
user_payload = build_user_payload(
|
|
identity: identity,
|
|
username: username,
|
|
user_type: user_type
|
|
)
|
|
|
|
retries = 0
|
|
max_retries = 1
|
|
response = nil
|
|
|
|
loop do
|
|
Rails.logger.info "Creating Slack user with payload: #{user_payload.inspect}"
|
|
response = client.post("Users", user_payload)
|
|
|
|
if response.success?
|
|
break
|
|
end
|
|
|
|
error_msg = if response.body.is_a?(Hash)
|
|
response.body.dig("Errors", 0, "description") ||
|
|
response.body["detail"] ||
|
|
response.body["message"] ||
|
|
response.body["error"]
|
|
end
|
|
|
|
error_msg ||= "Unknown error (Status #{response.status}): #{response.body.inspect}"
|
|
|
|
Rails.logger.error "Failed to create Slack user: #{error_msg}"
|
|
|
|
# Check for email_taken error with existing_user ID
|
|
if error_msg =~ /email_taken.*existing_user=(U[A-Z0-9]+)/i
|
|
existing_slack_id = $1
|
|
Rails.logger.info "Email taken, extracted existing Slack user ID: #{existing_slack_id}"
|
|
return {
|
|
success: true,
|
|
slack_id: existing_slack_id,
|
|
created: false,
|
|
message: "Linked existing Slack account (from error response)"
|
|
}
|
|
end
|
|
|
|
# Check if user already exists but wasn't found by email lookup
|
|
if error_msg.include?("already") || error_msg.include?("duplicate") || error_msg.include?("exists") || error_msg.include?("email_taken") || error_msg.include?("conflict")
|
|
# Try to find the existing user by email using SCIM
|
|
existing_user = find_existing_user_by_email(identity.primary_email)
|
|
|
|
# Fallback to Web API lookup if SCIM failed to find the user
|
|
existing_user ||= SlackService.find_by_email(identity.primary_email)
|
|
|
|
if existing_user
|
|
Rails.logger.info "Found existing Slack user for #{identity.primary_email}: #{existing_user}"
|
|
return {
|
|
success: true,
|
|
slack_id: existing_user,
|
|
created: false,
|
|
message: "Linked existing Slack account (found via lookup)"
|
|
}
|
|
end
|
|
end
|
|
|
|
# Retry once after 1 second if we get invited_user_not_created error
|
|
if error_msg.include?("invited_user_not_created") && retries < max_retries
|
|
Rails.logger.info "Got invited_user_not_created error, retrying after 1 second..."
|
|
sleep(1)
|
|
retries += 1
|
|
next
|
|
end
|
|
|
|
return {
|
|
success: false,
|
|
error: error_msg,
|
|
created: false
|
|
}
|
|
end
|
|
|
|
slack_id = response.body["id"]
|
|
|
|
if user_type == :multi_channel_guest
|
|
channel_ids = scenario.slack_channels if scenario.slack_channels.any?
|
|
sleep(2)
|
|
assigned = SlackService.assign_to_workspace(user_id: slack_id, user_type:, channel_ids:)
|
|
unless assigned
|
|
Sentry.capture_message(
|
|
"Slack workspace assignment failed after SCIM user creation",
|
|
level: :error,
|
|
extra: {
|
|
identity_id: identity.id,
|
|
identity_public_id: identity.public_id,
|
|
identity_email: identity.primary_email,
|
|
slack_id: slack_id,
|
|
user_type: user_type,
|
|
channel_ids: channel_ids,
|
|
scenario: scenario.class.name,
|
|
scenario_slug: scenario.class.slug,
|
|
onboarding_scenario: identity.onboarding_scenario,
|
|
team_id: SlackService.team_id
|
|
}
|
|
)
|
|
end
|
|
end
|
|
|
|
Rails.logger.info "Successfully created Slack user #{slack_id} for #{identity.primary_email}"
|
|
|
|
{
|
|
success: true,
|
|
slack_id:,
|
|
created: true,
|
|
username:,
|
|
user_type:,
|
|
message: "Created new Slack account"
|
|
}
|
|
rescue => e
|
|
Rails.logger.error "Error creating Slack user: #{e.message}"
|
|
Sentry.capture_exception(e,
|
|
level: :error,
|
|
tags: { component: "slack", critical: true, operation: "scim_create_user" },
|
|
extra: {
|
|
identity_public_id: identity.public_id,
|
|
identity_email: identity.primary_email
|
|
}
|
|
)
|
|
|
|
{
|
|
success: false,
|
|
error: e.message,
|
|
created: false
|
|
}
|
|
end
|
|
|
|
def scim_token = ENV["SLACK_SCIM_TOKEN"] || raise("SLACK_SCIM_TOKEN not configured")
|
|
|
|
def find_existing_user_by_email(email)
|
|
response = client.get("Users") do |req|
|
|
req.params["filter"] = "emails eq \"#{email}\""
|
|
end
|
|
|
|
response.body.dig("Resources", 0, "id")
|
|
rescue => e
|
|
Rails.logger.warn "Error finding existing user by email via SCIM: #{e.message}"
|
|
Sentry.capture_exception(e, tags: { component: "slack", operation: "scim_find_user" }, extra: { email: email })
|
|
nil
|
|
end
|
|
|
|
private
|
|
|
|
def client
|
|
@client ||= Faraday.new(url: SCIM_BASE_URL) do |f|
|
|
f.headers["Authorization"] = "Bearer #{scim_token}"
|
|
f.request :json
|
|
f.response :json, content_type: /\bjson$/
|
|
f.adapter Faraday.default_adapter
|
|
end
|
|
end
|
|
|
|
def username_exists?(username)
|
|
response = client.get("Users") do |req|
|
|
req.params["filter"] = "userName eq \"#{username}\""
|
|
end
|
|
|
|
response.body["Resources"]&.any? || false
|
|
rescue => e
|
|
Rails.logger.warn "Error checking username existence: #{e.message}"
|
|
Sentry.capture_exception(e, tags: { component: "slack", operation: "scim_check_username" }, extra: { username: username })
|
|
false
|
|
end
|
|
|
|
def generate_unique_username(email)
|
|
base_username = email.split("@").first.gsub(/[^a-z0-9_\-.]/, "").downcase[0..20]
|
|
username = base_username
|
|
counter = 1
|
|
|
|
while username_exists?(username)
|
|
suffix = counter.to_s
|
|
max_base_length = 21 - suffix.length
|
|
username = "#{base_username[0...max_base_length]}#{suffix}"
|
|
counter += 1
|
|
end
|
|
|
|
username
|
|
end
|
|
|
|
def build_user_payload(identity:, username:, user_type:)
|
|
payload = {
|
|
schemas: %w[urn:ietf:params:scim:schemas:core:2.0:User urn:ietf:params:scim:schemas:extension:enterprise:2.0:User urn:ietf:params:scim:schemas:extension:slack:profile:2.0:User],
|
|
userName: username,
|
|
name: {
|
|
givenName: identity.first_name,
|
|
familyName: identity.last_name
|
|
},
|
|
emails: [
|
|
{
|
|
value: identity.primary_email,
|
|
primary: true
|
|
}
|
|
],
|
|
"urn:ietf:params:scim:schemas:extension:slack:profile:2.0:User": {
|
|
startDate: Time.current.iso8601
|
|
}
|
|
}
|
|
|
|
# Add guest extension for multi-channel guests
|
|
if user_type == :multi_channel_guest
|
|
payload[:schemas] << "urn:ietf:params:scim:schemas:extension:slack:guest:2.0:User"
|
|
payload[:"urn:ietf:params:scim:schemas:extension:slack:guest:2.0:User"] = { type: "multi" }
|
|
end
|
|
|
|
payload
|
|
end
|
|
end
|
|
end
|