Merge pull request 'Add custom LDAP attributes to schema' (#181) from feature/custom_ldap_attributes into master
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #181
Reviewed-by: greg <greg@noreply.kosmos.org>
This commit is contained in:
Râu Cao 2024-03-19 14:46:44 +00:00
commit 51a3cb60ec
24 changed files with 317 additions and 117 deletions

View File

@ -12,7 +12,7 @@ class SettingsController < ApplicationController
end end
def show def show
if @settings_section == "experiments" if @settings_section == "nostr"
session[:shared_secret] ||= SecureRandom.base64(12) session[:shared_secret] ||= SecureRandom.base64(12)
end end
end end
@ -88,6 +88,7 @@ class SettingsController < ApplicationController
def set_nostr_pubkey def set_nostr_pubkey
signed_event = nostr_event_params[:signed_event].to_h.symbolize_keys signed_event = nostr_event_params[:signed_event].to_h.symbolize_keys
is_valid_id = NostrManager::ValidateId.call(event: signed_event) is_valid_id = NostrManager::ValidateId.call(event: signed_event)
is_valid_sig = NostrManager::VerifySignature.call(event: signed_event) is_valid_sig = NostrManager::VerifySignature.call(event: signed_event)
is_correct_content = signed_event[:content] == "Connect my public key to #{current_user.address} (confirmation #{session[:shared_secret]})" is_correct_content = signed_event[:content] == "Connect my public key to #{current_user.address} (confirmation #{session[:shared_secret]})"
@ -97,30 +98,26 @@ class SettingsController < ApplicationController
http_status :unprocessable_entity and return http_status :unprocessable_entity and return
end end
pubkey_taken = User.all_except(current_user).where( user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event[:pubkey])
ou: current_user.ou, nostr_pubkey: signed_event[:pubkey]
).any?
if pubkey_taken if user_with_pubkey.present? && (user_with_pubkey != current_user)
flash[:alert] = "Public key already in use for a different account" flash[:alert] = "Public key already in use for a different account"
http_status :unprocessable_entity and return http_status :unprocessable_entity and return
end end
current_user.update! nostr_pubkey: signed_event[:pubkey] LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event[:pubkey])
session[:shared_secret] = nil session[:shared_secret] = nil
flash[:success] = "Public key verification successful" flash[:success] = "Public key verification successful"
http_status :ok http_status :ok
rescue
flash[:alert] = "Public key could not be verified"
http_status :unprocessable_entity and return
end end
# DELETE /settings/nostr_pubkey # DELETE /settings/nostr_pubkey
def remove_nostr_pubkey def remove_nostr_pubkey
current_user.update! nostr_pubkey: nil # TODO require current pubkey or password to delete
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: nil)
redirect_to setting_path(:experiments), flash: { redirect_to setting_path(:nostr), flash: {
success: 'Public key removed from account' success: 'Public key removed from account'
} }
end end
@ -134,8 +131,8 @@ class SettingsController < ApplicationController
def set_settings_section def set_settings_section
@settings_section = params[:section] @settings_section = params[:section]
allowed_sections = [ allowed_sections = [
:profile, :account, :xmpp, :email, :lightning, :remotestorage, :profile, :account, :xmpp, :email,
:experiments :lightning, :remotestorage, :nostr
] ]
unless allowed_sections.include?(@settings_section.to_sym) unless allowed_sections.include?(@settings_section.to_sym)
@ -165,7 +162,7 @@ class SettingsController < ApplicationController
def nostr_event_params def nostr_event_params
params.permit(signed_event: [ params.permit(signed_event: [
:id, :pubkey, :created_at, :kind, :tags, :content, :sig :id, :pubkey, :created_at, :kind, :content, :sig, tags: []
]) ])
end end

View File

@ -50,8 +50,6 @@ class User < ApplicationRecord
validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true, validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true,
if: -> { defined?(@display_name) } if: -> { defined?(@display_name) }
validates_uniqueness_of :nostr_pubkey, allow_blank: true
validate :acceptable_avatar validate :acceptable_avatar
# #
@ -163,37 +161,41 @@ class User < ApplicationRecord
@display_name ||= ldap_entry[:display_name] @display_name ||= ldap_entry[:display_name]
end end
def nostr_pubkey
@nostr_pubkey ||= ldap_entry[:nostr_key]
end
def nostr_pubkey_bech32
return nil unless nostr_pubkey.present?
Nostr::PublicKey.new(nostr_pubkey).to_bech32
end
def avatar def avatar
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn) @avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
end end
def services_enabled def services_enabled
ldap_entry[:service] || [] ldap_entry[:services_enabled] || []
end end
def enable_service(service) def enable_service(service)
current_services = services_enabled current_services = services_enabled
new_services = Array(service).map(&:to_s) new_services = Array(service).map(&:to_s)
services = (current_services + new_services).uniq services = (current_services + new_services).uniq
ldap.replace_attribute(dn, :service, services) ldap.replace_attribute(dn, :serviceEnabled, services)
end end
def disable_service(service) def disable_service(service)
current_services = services_enabled current_services = services_enabled
disabled_services = Array(service).map(&:to_s) disabled_services = Array(service).map(&:to_s)
services = (current_services - disabled_services).uniq services = (current_services - disabled_services).uniq
ldap.replace_attribute(dn, :service, services) ldap.replace_attribute(dn, :serviceEnabled, services)
end end
def disable_all_services def disable_all_services
ldap.delete_attribute(dn,:service) ldap.delete_attribute(dn,:service)
end end
def nostr_pubkey_bech32
return nil unless nostr_pubkey.present?
Nostr::PublicKey.new(nostr_pubkey).to_bech32
end
private private
def ldap def ldap

View File

@ -9,7 +9,7 @@ module LdapManager
attributes = %w{ jpegPhoto } attributes = %w{ jpegPhoto }
filter = Net::LDAP::Filter.eq("cn", @cn) filter = Net::LDAP::Filter.eq("cn", @cn)
entry = ldap_client.search(base: treebase, filter: filter, attributes: attributes).first entry = client.search(base: treebase, filter: filter, attributes: attributes).first
entry.try(:jpegPhoto) ? entry.jpegPhoto.first : nil entry.try(:jpegPhoto) ? entry.jpegPhoto.first : nil
end end
end end

View File

@ -0,0 +1,18 @@
module LdapManager
class FetchUserByNostrKey < LdapManagerService
def initialize(pubkey:)
@ou = Setting.primary_domain
@pubkey = pubkey
end
def call
treebase = "ou=#{@ou},cn=users,#{ldap_suffix}"
attributes = %w{ cn }
filter = Net::LDAP::Filter.eq("nostrKey", @pubkey)
entry = client.search(base: treebase, filter: filter, attributes: attributes).first
User.find_by cn: entry.cn, ou: @ou unless entry.nil?
end
end
end

View File

@ -0,0 +1,16 @@
module LdapManager
class UpdateNostrKey < LdapManagerService
def initialize(dn:, pubkey:)
@dn = dn
@pubkey = pubkey
end
def call
if @pubkey.present?
replace_attribute @dn, :nostrKey, @pubkey
else
delete_attribute @dn, :nostrKey
end
end
end
end

View File

@ -1,5 +1,2 @@
class LdapManagerService < LdapService class LdapManagerService < LdapService
def suffix
@suffix ||= ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
end
end end

View File

@ -1,41 +1,47 @@
class LdapService < ApplicationService class LdapService < ApplicationService
def initialize def modify(dn, operations=[])
@suffix = ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org" client.modify dn: dn, operations: operations
client.get_operation_result.code
end end
def add_attribute(dn, attr, values) def add_attribute(dn, attr, values)
ldap_client.add_attribute dn, attr, values client.add_attribute dn, attr, values
client.get_operation_result.code
end end
def replace_attribute(dn, attr, values) def replace_attribute(dn, attr, values)
ldap_client.replace_attribute dn, attr, values client.replace_attribute dn, attr, values
client.get_operation_result.code
end end
def delete_attribute(dn, attr) def delete_attribute(dn, attr)
ldap_client.delete_attribute dn, attr client.delete_attribute dn, attr
client.get_operation_result.code
end end
def add_entry(dn, attrs, interactive=false) def add_entry(dn, attrs, interactive=false)
puts "Adding entry: #{dn}" if interactive puts "Add entry: #{dn}" if interactive
res = ldap_client.add dn: dn, attributes: attrs client.add dn: dn, attributes: attrs
puts res.inspect if interactive && !res client.get_operation_result.code
res
end end
def delete_entry(dn, interactive=false) def delete_entry(dn, interactive=false)
puts "Deleting entry: #{dn}" if interactive puts "Delete entry: #{dn}" if interactive
res = ldap_client.delete dn: dn client.delete dn: dn
puts res.inspect if interactive && !res client.get_operation_result.code
res
end end
def delete_all_entries! def delete_all_users!
delete_all_entries!(objectclass: "person")
end
def delete_all_entries!(objectclass: "*")
if Rails.env.production? if Rails.env.production?
raise "Mass deletion of entries not allowed in production" raise "Mass deletion of entries not allowed in production"
end end
filter = Net::LDAP::Filter.eq("objectClass", "*") filter = Net::LDAP::Filter.eq("objectClass", objectclass)
entries = ldap_client.search(base: @suffix, filter: filter, attributes: %w{dn}) entries = client.search(base: ldap_suffix, filter: filter, attributes: %w{dn})
entries.sort_by!{ |e| e.dn.length }.reverse! entries.sort_by!{ |e| e.dn.length }.reverse!
entries.each do |e| entries.each do |e|
@ -45,18 +51,18 @@ class LdapService < ApplicationService
def fetch_users(args={}) def fetch_users(args={})
if args[:ou] if args[:ou]
treebase = "ou=#{args[:ou]},cn=users,#{@suffix}" treebase = "ou=#{args[:ou]},cn=users,#{ldap_suffix}"
else else
treebase = ldap_config["base"] treebase = ldap_config["base"]
end end
attributes = %w[ attributes = %w[
dn cn uid mail displayName admin service dn cn uid mail displayName admin service
mailRoutingAddress mailpassword mailRoutingAddress mailpassword nostrKey
] ]
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*") filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes) entries = client.search(base: treebase, filter: filter, attributes: attributes)
entries.sort_by! { |e| e.cn[0] } entries.sort_by! { |e| e.cn[0] }
entries = entries.collect do |e| entries = entries.collect do |e|
{ {
@ -64,9 +70,10 @@ class LdapService < ApplicationService
mail: e.try(:mail) ? e.mail.first : nil, mail: e.try(:mail) ? e.mail.first : nil,
display_name: e.try(:displayName) ? e.displayName.first : nil, display_name: e.try(:displayName) ? e.displayName.first : nil,
admin: e.try(:admin) ? 'admin' : nil, admin: e.try(:admin) ? 'admin' : nil,
service: e.try(:service), services_enabled: e.try(:serviceEnabled),
email_maildrop: e.try(:mailRoutingAddress), email_maildrop: e.try(:mailRoutingAddress),
email_password: e.try(:mailpassword) email_password: e.try(:mailpassword),
nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil
} }
end end
end end
@ -75,9 +82,9 @@ class LdapService < ApplicationService
attributes = %w{dn ou description} attributes = %w{dn ou description}
filter = Net::LDAP::Filter.eq("objectClass", "organizationalUnit") filter = Net::LDAP::Filter.eq("objectClass", "organizationalUnit")
# filter = Net::LDAP::Filter.eq("objectClass", "*") # filter = Net::LDAP::Filter.eq("objectClass", "*")
treebase = "cn=users,#{@suffix}" treebase = "cn=users,#{ldap_suffix}"
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes) entries = client.search(base: treebase, filter: filter, attributes: attributes)
entries.sort_by! { |e| e.ou[0] } entries.sort_by! { |e| e.ou[0] }
@ -91,10 +98,10 @@ class LdapService < ApplicationService
end end
def add_organization(ou, description, interactive=false) def add_organization(ou, description, interactive=false)
dn = "ou=#{ou},cn=users,#{@suffix}" dn = "ou=#{ou},cn=users,#{ldap_suffix}"
aci = <<-EOS aci = <<-EOS
(target="ldap:///cn=*,ou=#{ou},cn=users,#{@suffix}")(targetattr="cn || sn || uid || mail || userPassword || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{@suffix}";) (target="ldap:///cn=*,ou=#{ou},cn=users,#{ldap_suffix}")(targetattr="cn || sn || uid || mail || userPassword || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{ldap_suffix}";)
EOS EOS
attrs = { attrs = {
@ -115,22 +122,22 @@ class LdapService < ApplicationService
delete_all_entries! delete_all_entries!
user_read_aci = <<-EOS user_read_aci = <<-EOS
(target="ldap:///#{@suffix}")(targetattr="*") (version 3.0; acl "user-read-search-own-attributes"; allow (read,search) userdn="ldap:///self";) (target="ldap:///#{ldap_suffix}")(targetattr="*") (version 3.0; acl "user-read-search-own-attributes"; allow (read,search) userdn="ldap:///self";)
EOS EOS
add_entry @suffix, { add_entry ldap_suffix, {
dc: "kosmos", objectClass: ["top", "domain"], aci: user_read_aci dc: "kosmos", objectClass: ["top", "domain"], aci: user_read_aci
}, true }, true
add_entry "cn=users,#{@suffix}", { add_entry "cn=users,#{ldap_suffix}", {
cn: "users", objectClass: ["top", "organizationalRole"] cn: "users", objectClass: ["top", "organizationalRole"]
}, true }, true
end end
private private
def ldap_client def client
ldap_client ||= Net::LDAP.new host: ldap_config['host'], client ||= Net::LDAP.new host: ldap_config['host'],
port: ldap_config['port'], port: ldap_config['port'],
# TODO has to be :simple_tls if TLS is enabled # TODO has to be :simple_tls if TLS is enabled
# encryption: ldap_config['ssl'], # encryption: ldap_config['ssl'],
@ -144,4 +151,8 @@ class LdapService < ApplicationService
def ldap_config def ldap_config
ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env] ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env]
end end
def ldap_suffix
@ldap_suffix ||= ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
end
end end

View File

@ -0,0 +1,6 @@
<svg width="24" height="24" class="icon-nostrich-head <%= custom_class %>" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.03377 4.84648C2.38935 5.60878 1.88639 6.49681 1.5799 7.4713C3.32454 7.07836 5.64286 6.98406 6.95527 6.88189C7.36392 5.20013 8.52701 3.91915 10.476 4.0056C11.3169 4.04489 12.0556 4.58714 12.5664 5.42017C12.9436 5.01937 13.4466 4.75218 14.1146 4.65787C14.1617 4.65787 14.2639 4.65001 14.3425 4.65001C12.9593 3.14114 10.9868 2.18237 8.77849 2.18237C8.3777 2.18237 7.98476 2.22167 7.59183 2.28454C7.51324 2.28454 7.41108 2.30026 7.27748 2.33169C7.26962 2.33169 7.2539 2.33169 7.24604 2.33169C7.23818 2.33169 7.23032 2.33169 7.21461 2.33169C5.69001 2.70105 4.54264 2.40242 3.89037 1.51438C3.81964 1.42008 3.54458 1.00357 3.45814 0.272705C2.97876 0.767805 2.66441 1.58511 2.9316 2.45743C3.14379 3.149 3.54458 3.51836 3.97681 3.73054C3.31668 3.76984 2.76657 3.6441 2.21646 3.22759C1.89425 2.98396 1.68992 2.71677 1.352 2.01734C1.03765 2.51244 1.06909 3.06255 1.13195 3.34547C1.21054 3.72268 1.40701 4.14706 1.65849 4.39068C2.04357 4.76789 2.59368 4.85434 3.04162 4.84648H3.03377Z" fill="currentColor"/>
<path d="M10.4837 11.3458C11.4602 11.3458 12.2519 9.99116 12.2519 8.32016C12.2519 6.64917 11.4602 5.29456 10.4837 5.29456C9.50711 5.29456 8.71545 6.64917 8.71545 8.32016C8.71545 9.99116 9.50711 11.3458 10.4837 11.3458Z" fill="currentColor"/>
<path d="M14.3737 10.615C15.1376 10.615 15.7569 9.53831 15.7569 8.21019C15.7569 6.88207 15.1376 5.80542 14.3737 5.80542C13.6099 5.80542 12.9906 6.88207 12.9906 8.21019C12.9906 9.53831 13.6099 10.615 14.3737 10.615Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.52542 23.9833C7.53337 23.6314 7.66454 22.5232 8.7864 20.3047C9.2815 19.3381 10.4053 18.0021 11.2462 17.2791C11.6941 16.8862 12.1421 16.5561 12.5822 16.2496C12.8101 16.116 13.0222 15.9745 13.2266 15.8252C16.9076 13.5684 20.157 14.0396 22.8528 14.4306L22.9321 14.4421C22.9321 14.4421 23.5765 12.5246 20.9203 11.5344C19.4743 11 17.7689 10.5677 16.3465 10.2691C16.1422 10.6385 15.8828 10.9528 15.5763 11.1886C15.5721 11.1917 15.5678 11.195 15.5634 11.1983C15.3354 11.3696 14.795 11.7757 13.816 11.6601C13.313 11.5972 12.9279 11.3929 12.6215 11.0943C12.1028 11.9509 11.3562 12.5088 10.4917 12.5874C8.09483 12.7918 6.88458 10.7799 6.806 8.55591C5.00635 8.7288 2.55443 9.83688 1.24988 10.4813L1.25662 22.0396C2.92115 22.6846 5.41819 23.4807 7.52542 23.9833Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" class="icon-nostrich-n <%= custom_class %>" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24 10.4604V23.135C24 23.6117 23.6161 23.9985 23.1429 23.9985H12.8578C12.3847 23.9985 12.0008 23.6117 12.0008 23.135V20.7746C12.0476 17.8812 12.3515 15.1096 12.9894 13.8487C13.3718 13.0904 14.0021 12.6777 14.7262 12.4569C16.0942 12.0426 18.4947 12.3259 19.5135 12.2772C19.5135 12.2772 22.5912 12.4005 22.5912 10.6447C22.5912 9.23147 21.2156 9.34264 21.2156 9.34264C19.6994 9.38223 18.5446 9.27868 17.7963 8.98173C16.5432 8.48528 16.5009 7.57462 16.4963 7.27005C16.4343 3.75228 11.2858 3.33046 6.74939 4.20305C1.78976 5.1533 6.80381 12.3152 6.80381 21.8756V23.1518C6.79474 23.6208 6.41834 24 5.94974 24H0.857089C0.383951 24 0 23.6132 0 23.1365V1.21523C0 0.738579 0.383951 0.351777 0.857089 0.351777H5.64439C6.11753 0.351777 6.50148 0.738579 6.50148 1.21523C6.50148 1.92335 7.29206 2.31777 7.86345 1.90508C9.58519 0.662437 11.7952 0 14.2682 0C19.8083 0 23.997 3.25279 23.997 10.4604H24ZM14.8033 7.88832C14.8033 6.86802 13.9825 6.04112 12.9697 6.04112C11.9569 6.04112 11.1361 6.86802 11.1361 7.88832C11.1361 8.90863 11.9569 9.73553 12.9697 9.73553C13.9825 9.73553 14.8033 8.90863 14.8033 7.88832Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" class="icon-nostrich <%= custom_class %>" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.7084 10.1607C18.1683 13.3466 14.8705 14.0207 12.9733 13.9618C12.8515 13.958 12.7366 14.0173 12.6647 14.1157C12.4684 14.384 12.1547 14.7309 11.9125 14.7309C11.6405 14.7309 11.3957 15.254 11.284 15.5795C11.2723 15.6137 11.3059 15.6452 11.3403 15.634C14.345 14.6584 15.5241 14.3238 16.032 14.4178C16.4421 14.4937 17.209 15.8665 17.5413 16.5434C16.7155 16.5909 16.4402 15.8507 16.2503 15.7178C16.0985 15.6116 16.0415 16.0974 16.032 16.3536C15.8517 16.2587 15.6239 16.1259 15.6049 15.7178C15.5859 15.3098 15.3771 15.4142 15.2157 15.4332C15.0544 15.4521 12.5769 16.2493 12.2067 16.3536C11.8366 16.458 11.4094 16.6004 11.0582 16.8471C10.4697 17.1318 10.09 16.9325 9.98561 16.4485C9.90208 16.0614 10.4444 14.8701 10.726 14.3229C10.3779 14.4526 9.65529 14.7158 9.54898 14.7309C9.44588 14.7457 8.13815 15.7552 7.43879 16.3038C7.398 16.3358 7.37174 16.3827 7.36236 16.4336C7.25047 17.0416 6.89335 17.2118 6.27423 17.5303C5.77602 17.7867 4.036 20.4606 3.14127 21.9041C3.0794 22.0039 2.9886 22.0806 2.8911 22.1461C2.32279 22.5276 1.74399 23.4985 1.50923 23.9737C1.17511 23.0095 1.61048 22.1802 1.86993 21.886C1.75602 21.7873 1.49341 21.8449 1.37634 21.886C1.69907 20.7757 2.82862 20.7757 2.79066 20.7757C2.99948 20.5954 5.44842 17.0938 5.50538 16.9325C5.56187 16.7725 5.46892 16.0242 6.69975 15.6139C6.7193 15.6073 6.73868 15.5984 6.75601 15.5873C7.71493 14.971 8.43427 13.9774 8.67571 13.5542C7.39547 13.4662 5.92943 12.7525 5.16289 12.294C4.99765 12.1952 4.8224 12.1092 4.63108 12.0875C3.58154 11.9687 2.53067 12.6401 2.10723 13.0228C1.93258 12.7799 2.12938 12.0739 2.24961 11.7513C1.82437 11.6905 1.19916 12.308 0.939711 12.6243C0.658747 12.184 0.904907 11.397 1.06311 11.0585C0.501179 11.0737 0.120232 11.3306 0 11.4571C0.465109 7.99343 4.02275 9.00076 4.06259 9.04675C3.87275 8.84937 3.88857 8.59126 3.92021 8.48688C6.0749 8.54381 7.08105 8.18321 7.71702 7.81313C12.7288 5.01374 14.8882 6.73133 15.6856 7.1631C16.4829 7.59487 17.9304 7.77042 18.9318 7.37187C20.1278 6.83097 19.9478 5.43673 19.7054 4.90461C19.4397 4.32101 17.9399 3.51438 17.4084 2.49428C16.8768 1.47418 17.34 0.233672 17.9558 0.0607684C18.5425 -0.103972 18.9615 0.0876835 19.2831 0.378128C19.4974 0.571763 20.0994 0.710259 20.3509 0.800409C20.6024 0.890558 21.0201 1.00918 20.9964 1.08035C20.9726 1.15152 20.5699 1.14202 20.5075 1.14202C20.3794 1.14202 20.2275 1.161 20.3794 1.23217C20.5575 1.30439 20.8263 1.40936 20.955 1.47846C20.9717 1.48744 20.9683 1.51084 20.95 1.51577C20.0765 1.75085 19.2966 1.26578 18.7183 1.82526C18.1298 2.39463 19.3827 2.83114 20.0282 3.51438C20.6736 4.19762 21.3381 5.01372 20.8065 6.87365C20.395 8.31355 18.6703 9.53781 17.7795 10.0167C17.7282 10.0442 17.7001 10.1031 17.7084 10.1607Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -34,7 +34,7 @@
<% end %> <% end %>
<% if Setting.nostr_enabled %> <% if Setting.nostr_enabled %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
name: "Experiments", path: setting_path(:experiments), icon: "science", name: "Nostr", path: setting_path(:nostr), icon: "nostrich-head",
active: @settings_section.to_s == "experiments" active: @settings_section.to_s == "nostr"
) %> ) %>
<% end %> <% end %>

View File

@ -27,7 +27,6 @@ Devise.setup do |config|
config.ldap_auth_password_builder = Proc.new() { |new_password| config.ldap_auth_password_builder = Proc.new() { |new_password|
salt = SecureRandom.hex(32) salt = SecureRandom.hex(32)
hashed_pw = Base64.strict_encode64(Digest::SHA512.digest(new_password + salt) + salt) hashed_pw = Base64.strict_encode64(Digest::SHA512.digest(new_password + salt) + salt)
puts '{SSHA512}' + hashed_pw
'{SSHA512}' + hashed_pw '{SSHA512}' + hashed_pw
} }

View File

@ -0,0 +1,5 @@
class RemoveNostrPubkeyFromUsers < ActiveRecord::Migration[7.1]
def change
remove_column :users, :nostr_pubkey, :string
end
end

View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_02_16_124640) do ActiveRecord::Schema[7.1].define(version: 2024_03_16_153558) do
create_table "active_storage_attachments", force: :cascade do |t| create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "record_type", null: false t.string "record_type", null: false
@ -129,7 +129,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_02_16_124640) do
t.string "unconfirmed_email" t.string "unconfirmed_email"
t.text "ln_password_ciphertext" t.text "ln_password_ciphertext"
t.string "ln_account" t.string "ln_account"
t.string "nostr_pubkey"
t.datetime "remember_created_at" t.datetime "remember_created_at"
t.string "remember_token" t.string "remember_token"
t.text "preferences" t.text "preferences"

View File

@ -3,6 +3,10 @@ require 'sidekiq/testing'
ldap = LdapService.new ldap = LdapService.new
Sidekiq::Testing.inline! do Sidekiq::Testing.inline! do
ldap.delete_all_users!
puts "Create user: admin"
CreateAccount.call(account: { CreateAccount.call(account: {
username: "admin", domain: "kosmos.org", email: "admin@example.com", username: "admin", domain: "kosmos.org", email: "admin@example.com",
password: "admin is admin", confirmed: true password: "admin is admin", confirmed: true
@ -10,6 +14,7 @@ Sidekiq::Testing.inline! do
ldap.add_attribute "cn=admin,ou=kosmos.org,cn=users,dc=kosmos,dc=org", :admin, "true" ldap.add_attribute "cn=admin,ou=kosmos.org,cn=users,dc=kosmos,dc=org", :admin, "true"
puts "Create 35 random users"
35.times do |n| 35.times do |n|
username = Faker::Name.unique.first_name.downcase username = Faker::Name.unique.first_name.downcase
email = Faker::Internet.unique.email email = Faker::Internet.unique.email

View File

@ -1,6 +1,6 @@
namespace :ldap do namespace :ldap do
desc "Reset the LDAP directory and set up base entries and default org" desc "Reset the LDAP directory and set up base entries and default org"
task setup: :environment do |t, args| task setup: [:environment, :add_custom_attributes] do |t, args|
ldap = LdapService.new ldap = LdapService.new
ldap.delete_entry "cn=admin_role,ou=kosmos.org,cn=users,dc=kosmos,dc=org", true ldap.delete_entry "cn=admin_role,ou=kosmos.org,cn=users,dc=kosmos,dc=org", true
@ -19,6 +19,42 @@ namespace :ldap do
}, true }, true
end end
desc "Add custom attributes to schema"
task add_custom_attributes: :environment do |t, args|
%w[ admin service_enabled nostr_key ].each do |name|
Rake::Task["ldap:modify_ldap_schema"].invoke(name, "add")
Rake::Task['ldap:modify_ldap_schema'].reenable
end
end
desc "Delete custom attributes from schema"
task delete_custom_attributes: :environment do |t, args|
%w[ admin service_enabled nostr_key ].each do |name|
Rake::Task["ldap:modify_ldap_schema"].invoke(name, "delete")
Rake::Task['ldap:modify_ldap_schema'].reenable
end
end
desc "Modify LDAP schema"
task :modify_ldap_schema, [:name, :operation] => [:environment] do |t, args|
puts "Modify schema: #{args[:operation]} #{args[:name]}"
filename = "#{Rails.root}/schemas/ldap/#{args[:name]}.ldif"
ldif = YAML.safe_load(File.read(filename))
dn = ldif["dn"]
attribute = ldif["add"]
value = ldif[attribute]
operation = [ args[:operation].to_sym, attribute.to_sym, value ]
ldap = LdapService.new
res = ldap.modify dn, [ operation ]
if res != 0
puts "Result code: #{res}"
exit 1
end
end
desc "List user domains/organizations" desc "List user domains/organizations"
task list_organizations: :environment do |t, args| task list_organizations: :environment do |t, args|
ldap = LdapService.new ldap = LdapService.new

9
schemas/ldap/admin.ldif Normal file
View File

@ -0,0 +1,9 @@
dn: cn=schema
changetype: modify
add: attributeTypes
attributeTypes: ( 1.3.6.1.4.1.61554.1.1.2.1.1
NAME 'admin'
DESC 'Admin flag'
EQUALITY booleanMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
SINGLE-VALUE )

View File

@ -0,0 +1,9 @@
dn: cn=schema
changetype: modify
add: attributeTypes
attributeTypes: ( 1.3.6.1.4.1.61554.1.1.2.1.21
NAME 'nostrKey'
DESC 'Nostr public key'
EQUALITY caseIgnoreMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )

View File

@ -0,0 +1,8 @@
dn: cn=schema
changetype: modify
add: attributeTypes
attributeTypes: ( 1.3.6.1.4.1.61554.1.1.2.1.2
NAME 'serviceEnabled'
DESC 'Services enabled for account'
EQUALITY caseExactMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )

View File

@ -1,15 +1,19 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe 'Experimental Settings', type: :feature do RSpec.describe 'Nostr Settings', type: :feature do
let(:user) { create :user, cn: 'jimmy', ou: 'kosmos.org' } let(:user) { create :user, cn: 'jimmy', ou: 'kosmos.org' }
before do before do
login_as user, scope: :user login_as user, scope: :user
allow_any_instance_of(User).to receive(:dn)
.and_return("cn=#{user.cn},ou=kosmos.org,cn=users,dc=kosmos,dc=org")
allow_any_instance_of(User).to receive(:nostr_pubkey).and_return(nil)
end end
describe 'Adding a nostr pubkey' do describe 'Adding a nostr pubkey' do
scenario 'Without nostr browser extension available' do scenario 'Without nostr browser extension available' do
visit setting_path(:experiments) visit setting_path(:nostr)
expect(page).to have_content("No browser extension found") expect(page).to have_content("No browser extension found")
expect(page).to have_css('button[data-settings--nostr-pubkey-target=setPubkey]:disabled') expect(page).to have_css('button[data-settings--nostr-pubkey-target=setPubkey]:disabled')
end end
@ -22,19 +26,22 @@ RSpec.describe 'Experimental Settings', type: :feature do
context "With pubkey configured" do context "With pubkey configured" do
before do before do
user.update! nostr_pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" allow_any_instance_of(User).to receive(:nostr_pubkey)
.and_return("ce273cbfb0d4e3e06930773a337c1459e4849efd4cb4c751b906a561c98a6d09")
end end
scenario 'Remove nostr pubkey from account' do scenario 'Remove nostr pubkey from account' do
visit setting_path(:experiments) visit setting_path(:nostr)
expect(page).to have_field("nostr_public_key", expect(page).to have_field("nostr_public_key",
with: "npub1qlsc3g0lsl8pw8230w8d9wm6xxcax3f6pkemz5measrmwfxjxteslf2hac", with: "npub1ecnne0as6n37q6fswuarxlq5t8jgf8hafj6vw5deq6jkrjv2d5ysnehu73",
disabled: true) disabled: true)
expect(LdapManager::UpdateNostrKey).to receive(:call).with(
dn: "cn=jimmy,ou=kosmos.org,cn=users,dc=kosmos,dc=org",
pubkey: nil
)
click_link "Remove" click_link "Remove"
expect(page).to_not have_field("nostr_public_key")
expect(page).to have_content("verify your public key")
expect(user.reload.nostr_pubkey).to be_nil
end end
end end
end end

View File

@ -66,7 +66,7 @@ RSpec.describe User, type: :model do
it "returns the entries from the LDAP service attribute" do it "returns the entries from the LDAP service attribute" do
expect(user).to receive(:ldap_entry).and_return({ expect(user).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, mail: user.email, admin: nil, uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
service: ["discourse", "email", "gitea", "wiki", "xmpp"] services_enabled: ["discourse", "email", "gitea", "wiki", "xmpp"]
}) })
expect(user.services_enabled).to eq(["discourse", "email", "gitea", "wiki", "xmpp"]) expect(user.services_enabled).to eq(["discourse", "email", "gitea", "wiki", "xmpp"])
end end
@ -76,21 +76,21 @@ RSpec.describe User, type: :model do
before do before do
allow(user).to receive(:ldap_entry).and_return({ allow(user).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, mail: user.email, admin: nil, uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
service: ["discourse", "gitea"] services_enabled: ["discourse", "gitea"]
}) })
allow(user).to receive(:dn).and_return(dn) allow(user).to receive(:dn).and_return(dn)
end end
it "adds the service to the LDAP entry" do it "adds the service to the LDAP entry" do
expect_any_instance_of(LdapService).to receive(:replace_attribute) expect_any_instance_of(LdapService).to receive(:replace_attribute)
.with(dn, :service, ["discourse", "gitea", "wiki"]).and_return(true) .with(dn, :serviceEnabled, ["discourse", "gitea", "wiki"]).and_return(true)
user.enable_service(:wiki) user.enable_service(:wiki)
end end
it "adds multiple service to the LDAP entry" do it "adds multiple service to the LDAP entry" do
expect_any_instance_of(LdapService).to receive(:replace_attribute) expect_any_instance_of(LdapService).to receive(:replace_attribute)
.with(dn, :service, ["discourse", "gitea", "wiki", "xmpp"]).and_return(true) .with(dn, :serviceEnabled, ["discourse", "gitea", "wiki", "xmpp"]).and_return(true)
user.enable_service([:wiki, :xmpp]) user.enable_service([:wiki, :xmpp])
end end
@ -100,21 +100,21 @@ RSpec.describe User, type: :model do
before do before do
allow(user).to receive(:ldap_entry).and_return({ allow(user).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, mail: user.email, admin: nil, uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
service: ["discourse", "gitea", "xmpp"] services_enabled: ["discourse", "gitea", "xmpp"]
}) })
allow(user).to receive(:dn).and_return(dn) allow(user).to receive(:dn).and_return(dn)
end end
it "removes the service from the LDAP entry" do it "removes the service from the LDAP entry" do
expect_any_instance_of(LdapService).to receive(:replace_attribute) expect_any_instance_of(LdapService).to receive(:replace_attribute)
.with(dn, :service, ["discourse", "gitea"]).and_return(true) .with(dn, :serviceEnabled, ["discourse", "gitea"]).and_return(true)
user.disable_service(:xmpp) user.disable_service(:xmpp)
end end
it "removes multiple services from the LDAP entry" do it "removes multiple services from the LDAP entry" do
expect_any_instance_of(LdapService).to receive(:replace_attribute) expect_any_instance_of(LdapService).to receive(:replace_attribute)
.with(dn, :service, ["discourse"]).and_return(true) .with(dn, :serviceEnabled, ["discourse"]).and_return(true)
user.disable_service([:xmpp, "gitea"]) user.disable_service([:xmpp, "gitea"])
end end
@ -206,9 +206,21 @@ RSpec.describe User, type: :model do
end end
end end
describe "#nostr_pubkey" do
before do
allow_any_instance_of(User).to receive(:ldap_entry)
.and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" })
end
it "returns the raw pubkey from LDAP" do
expect(user.nostr_pubkey).to eq("07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3")
end
end
describe "#nostr_pubkey_bech32" do describe "#nostr_pubkey_bech32" do
before do before do
user.update! nostr_pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" allow_any_instance_of(User).to receive(:ldap_entry)
.and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" })
end end
it "encodes the hexadecimal pubkey to bech32" do it "encodes the hexadecimal pubkey to bech32" do

View File

@ -2,14 +2,23 @@ require 'rails_helper'
RSpec.describe "Settings", type: :request do RSpec.describe "Settings", type: :request do
let(:user) { create :user, cn: 'mark', ou: 'kosmos.org' } let(:user) { create :user, cn: 'mark', ou: 'kosmos.org' }
let(:other_user) { create :user, id: 2, cn: 'markymark', ou: 'kosmos.org', email: 'markymark@interscope.com' }
before do before do
login_as user, :scope => :user login_as user, :scope => :user
allow_any_instance_of(User).to receive(:dn)
.and_return("cn=#{user.cn},ou=kosmos.org,cn=users,dc=kosmos,dc=org")
allow_any_instance_of(User).to receive(:nostr_pubkey).and_return(nil)
allow(LdapManager::FetchUserByNostrKey).to receive(:call).with(
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
).and_return(nil)
end end
describe "GET /settings/experiments" do describe "GET /settings/nostr" do
it "works" do it "works" do
get setting_path(:experiments) get setting_path(:nostr)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
end end
@ -22,6 +31,11 @@ RSpec.describe "Settings", type: :request do
context "With valid data" do context "With valid data" do
before do before do
expect(LdapManager::UpdateNostrKey).to receive(:call).with(
dn: "cn=mark,ou=kosmos.org,cn=users,dc=kosmos,dc=org",
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
).and_return(0)
post set_nostr_pubkey_settings_path, params: { post set_nostr_pubkey_settings_path, params: {
signed_event: { signed_event: {
id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3", id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3",
@ -41,41 +55,17 @@ RSpec.describe "Settings", type: :request do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it "saves the pubkey" do it "informs the user about the success" do
expect(user.nostr_pubkey).to eq("07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3") expect(flash[:success]).to eq("Public key verification successful")
end end
end end
context "With wrong username" do context "With key already in use by someone else" do
before do before do
post set_nostr_pubkey_settings_path, params: { expect(LdapManager::FetchUserByNostrKey).to receive(:call).with(
signed_event: { pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
id: "2e1e20ee762d6a5b5b30835eda9ca03146e4baf82490e53fd75794c08de08ac0", ).and_return(other_user)
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3", expect(LdapManager::UpdateNostrKey).not_to receive(:call)
created_at: 1678255391,
kind: 1,
content: "Connect my public key to admin@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
sig: "2ace19c9db892ac6383848721a3e08b13d90d689fdeac60d9633a623d3f08eb7e0d468f1b3e928d1ea979477c2ec46ee6cdb2d053ef2e4ed3c0630a51d249029"
}
}.to_json, headers: {
"CONTENT_TYPE" => "application/json",
"HTTP_ACCEPT" => "application/json"
}
end
it "returns a 422 status" do
expect(response).to have_http_status(422)
end
it "does not save the pubkey" do
expect(user.nostr_pubkey).to be_nil
end
end
context "With wrong shared secret" do
before do
session_stub = { shared_secret: "ho-chi-minh" }
allow_any_instance_of(SettingsController).to receive(:session).and_return(session_stub)
post set_nostr_pubkey_settings_path, params: { post set_nostr_pubkey_settings_path, params: {
signed_event: { signed_event: {
@ -96,8 +86,67 @@ RSpec.describe "Settings", type: :request do
expect(response).to have_http_status(422) expect(response).to have_http_status(422)
end end
it "does not save the pubkey" do it "informs the user about the failure" do
expect(user.nostr_pubkey).to be_nil expect(flash[:alert]).to eq("Public key already in use for a different account")
end
end
context "With wrong username" do
before do
expect(LdapManager::UpdateNostrKey).not_to receive(:call)
post set_nostr_pubkey_settings_path, params: {
signed_event: {
id: "2e1e20ee762d6a5b5b30835eda9ca03146e4baf82490e53fd75794c08de08ac0",
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
created_at: 1678255391,
kind: 1,
content: "Connect my public key to admin@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
sig: "2ace19c9db892ac6383848721a3e08b13d90d689fdeac60d9633a623d3f08eb7e0d468f1b3e928d1ea979477c2ec46ee6cdb2d053ef2e4ed3c0630a51d249029"
}
}.to_json, headers: {
"CONTENT_TYPE" => "application/json",
"HTTP_ACCEPT" => "application/json"
}
end
it "returns a 422 status" do
expect(response).to have_http_status(422)
end
it "informs the user about the failure" do
expect(flash[:alert]).to eq("Public key could not be verified")
end
end
context "With wrong shared secret" do
before do
session_stub = { shared_secret: "ho-chi-minh" }
allow_any_instance_of(SettingsController).to receive(:session).and_return(session_stub)
expect(LdapManager::UpdateNostrKey).not_to receive(:call)
post set_nostr_pubkey_settings_path, params: {
signed_event: {
id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3",
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
created_at: 1678254161,
kind: 1,
content: "Connect my public key to mark@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
sig: "96796d420547d6e2c7be5de82a2ce7a48be99aac6415464a6081859ac1a9017305accc0228c630466a57d45ec1c3b456376eb538b76dfdaa2397e3258be02fdd"
}
}.to_json, headers: {
"CONTENT_TYPE" => "application/json",
"HTTP_ACCEPT" => "application/json"
}
end
it "returns a 422 status" do
expect(response).to have_http_status(422)
end
it "informs the user about the failure" do
expect(flash[:alert]).to eq("Public key could not be verified")
end end
end end
end end

View File

@ -19,6 +19,11 @@ RSpec.describe "Well-known URLs", type: :request do
context "user does not have a nostr pubkey configured" do context "user does not have a nostr pubkey configured" do
let(:user) { create :user, cn: 'spongebob', ou: 'kosmos.org' } let(:user) { create :user, cn: 'spongebob', ou: 'kosmos.org' }
before do
allow_any_instance_of(User).to receive(:ldap_entry)
.and_return({ nostr_key: nil })
end
it "returns a 404 status" do it "returns a 404 status" do
get "/.well-known/nostr.json?name=spongebob" get "/.well-known/nostr.json?name=spongebob"
expect(response).to have_http_status(:not_found) expect(response).to have_http_status(:not_found)
@ -26,8 +31,12 @@ RSpec.describe "Well-known URLs", type: :request do
end end
context "user with nostr pubkey" do context "user with nostr pubkey" do
let(:user) { create :user, cn: 'bobdylan', ou: 'kosmos.org', nostr_pubkey: '438d35a6750d0dd6b75d032af8a768aad76b62f0c70ecb45f9c4d9e63540f7f4' } let(:user) { create :user, cn: 'bobdylan', ou: 'kosmos.org' }
before { user.save! } before do
user.save!
allow_any_instance_of(User).to receive(:nostr_pubkey)
.and_return('438d35a6750d0dd6b75d032af8a768aad76b62f0c70ecb45f9c4d9e63540f7f4')
end
it "returns a NIP-05 response" do it "returns a NIP-05 response" do
get "/.well-known/nostr.json?name=bobdylan" get "/.well-known/nostr.json?name=bobdylan"