From 38b3d68fd517d2315cc4d3075875e5df6b5a1934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 13 Mar 2024 14:26:44 +0100 Subject: [PATCH 1/8] LDAP: Rename client method, add modify method --- app/services/ldap_manager/fetch_avatar.rb | 2 +- app/services/ldap_service.rb | 39 +++++++++++++---------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/app/services/ldap_manager/fetch_avatar.rb b/app/services/ldap_manager/fetch_avatar.rb index c2643f7..11035ae 100644 --- a/app/services/ldap_manager/fetch_avatar.rb +++ b/app/services/ldap_manager/fetch_avatar.rb @@ -9,7 +9,7 @@ module LdapManager attributes = %w{ jpegPhoto } 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 end end diff --git a/app/services/ldap_service.rb b/app/services/ldap_service.rb index 1c56df6..a286314 100644 --- a/app/services/ldap_service.rb +++ b/app/services/ldap_service.rb @@ -3,30 +3,37 @@ class LdapService < ApplicationService @suffix = ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org" end + def modify(dn, operations=[]) + client.modify dn: dn, operations: operations + client.get_operation_result.code + end + 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 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 def delete_attribute(dn, attr) - ldap_client.delete_attribute dn, attr + client.delete_attribute dn, attr + client.get_operation_result.code end def add_entry(dn, attrs, interactive=false) - puts "Adding entry: #{dn}" if interactive - res = ldap_client.add dn: dn, attributes: attrs - puts res.inspect if interactive && !res - res + puts "Add entry: #{dn}" if interactive + client.add dn: dn, attributes: attrs + client.get_operation_result.code end def delete_entry(dn, interactive=false) - puts "Deleting entry: #{dn}" if interactive - res = ldap_client.delete dn: dn - puts res.inspect if interactive && !res - res + puts "Delete entry: #{dn}" if interactive + client.delete dn: dn + client.get_operation_result.code + end end def delete_all_entries! @@ -35,7 +42,7 @@ class LdapService < ApplicationService end filter = Net::LDAP::Filter.eq("objectClass", "*") - entries = ldap_client.search(base: @suffix, filter: filter, attributes: %w{dn}) + entries = client.search(base: @suffix, filter: filter, attributes: %w{dn}) entries.sort_by!{ |e| e.dn.length }.reverse! entries.each do |e| @@ -56,7 +63,7 @@ class LdapService < ApplicationService ] 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 = entries.collect do |e| { @@ -77,7 +84,7 @@ class LdapService < ApplicationService # filter = Net::LDAP::Filter.eq("objectClass", "*") treebase = "cn=users,#{@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] } @@ -129,8 +136,8 @@ class LdapService < ApplicationService private - def ldap_client - ldap_client ||= Net::LDAP.new host: ldap_config['host'], + def client + client ||= Net::LDAP.new host: ldap_config['host'], port: ldap_config['port'], # TODO has to be :simple_tls if TLS is enabled # encryption: ldap_config['ssl'], From 06521d1c347265afef5a2bf0e5f37d397fc2c5f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 13 Mar 2024 14:27:39 +0100 Subject: [PATCH 2/8] LDAP: add delete_all_users method, use in seeds --- app/services/ldap_service.rb | 7 +++++-- db/seeds.rb | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/services/ldap_service.rb b/app/services/ldap_service.rb index a286314..91b6dd9 100644 --- a/app/services/ldap_service.rb +++ b/app/services/ldap_service.rb @@ -34,14 +34,17 @@ class LdapService < ApplicationService client.delete dn: dn client.get_operation_result.code end + + def delete_all_users! + delete_all_entries!(objectclass: "person") end - def delete_all_entries! + def delete_all_entries!(objectclass: "*") if Rails.env.production? raise "Mass deletion of entries not allowed in production" end - filter = Net::LDAP::Filter.eq("objectClass", "*") + filter = Net::LDAP::Filter.eq("objectClass", objectclass) entries = client.search(base: @suffix, filter: filter, attributes: %w{dn}) entries.sort_by!{ |e| e.dn.length }.reverse! diff --git a/db/seeds.rb b/db/seeds.rb index fa957a6..4c27d94 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -3,6 +3,8 @@ require 'sidekiq/testing' ldap = LdapService.new Sidekiq::Testing.inline! do + ldap.delete_all_users! + CreateAccount.call(account: { username: "admin", domain: "kosmos.org", email: "admin@example.com", password: "admin is admin", confirmed: true From 8de0a2e26e678474bf6cb044392055778db1057b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 13 Mar 2024 14:28:31 +0100 Subject: [PATCH 3/8] Improve seed output --- config/initializers/devise.rb | 1 - db/seeds.rb | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index d59cdd9..7930b83 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -27,7 +27,6 @@ Devise.setup do |config| config.ldap_auth_password_builder = Proc.new() { |new_password| salt = SecureRandom.hex(32) hashed_pw = Base64.strict_encode64(Digest::SHA512.digest(new_password + salt) + salt) - puts '{SSHA512}' + hashed_pw '{SSHA512}' + hashed_pw } diff --git a/db/seeds.rb b/db/seeds.rb index 4c27d94..5153b2b 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -5,6 +5,8 @@ ldap = LdapService.new Sidekiq::Testing.inline! do ldap.delete_all_users! + puts "Create user: admin" + CreateAccount.call(account: { username: "admin", domain: "kosmos.org", email: "admin@example.com", password: "admin is admin", confirmed: true @@ -12,6 +14,7 @@ Sidekiq::Testing.inline! do 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| username = Faker::Name.unique.first_name.downcase email = Faker::Internet.unique.email From de20931d305d4698517ab9b013da07c243c65e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 13 Mar 2024 14:29:14 +0100 Subject: [PATCH 4/8] Add tasks for modifying schema, first custom attributes refs #172, #173 --- lib/tasks/ldap.rake | 38 ++++++++++++++++++++++++++++++- schemas/ldap/admin.ldif | 9 ++++++++ schemas/ldap/nostr_key.ldif | 9 ++++++++ schemas/ldap/service_enabled.ldif | 8 +++++++ 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 schemas/ldap/admin.ldif create mode 100644 schemas/ldap/nostr_key.ldif create mode 100644 schemas/ldap/service_enabled.ldif diff --git a/lib/tasks/ldap.rake b/lib/tasks/ldap.rake index dcdebfe..3beb0e9 100644 --- a/lib/tasks/ldap.rake +++ b/lib/tasks/ldap.rake @@ -1,6 +1,6 @@ namespace :ldap do 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.delete_entry "cn=admin_role,ou=kosmos.org,cn=users,dc=kosmos,dc=org", true @@ -19,6 +19,42 @@ namespace :ldap do }, true 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" task list_organizations: :environment do |t, args| ldap = LdapService.new diff --git a/schemas/ldap/admin.ldif b/schemas/ldap/admin.ldif new file mode 100644 index 0000000..a667c8b --- /dev/null +++ b/schemas/ldap/admin.ldif @@ -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 ) diff --git a/schemas/ldap/nostr_key.ldif b/schemas/ldap/nostr_key.ldif new file mode 100644 index 0000000..22e7510 --- /dev/null +++ b/schemas/ldap/nostr_key.ldif @@ -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 ) diff --git a/schemas/ldap/service_enabled.ldif b/schemas/ldap/service_enabled.ldif new file mode 100644 index 0000000..7f80d4b --- /dev/null +++ b/schemas/ldap/service_enabled.ldif @@ -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 ) From 4217ba52e071b7415cc47db84ab76949367994e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 13 Mar 2024 16:41:49 +0100 Subject: [PATCH 5/8] Switch `service` LDAP attribute to `serviceEnabled` Improve internal naming on the way --- app/models/user.rb | 6 +++--- app/services/ldap_service.rb | 5 +++-- spec/models/user_spec.rb | 14 +++++++------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 9620bfe..fb8d95a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -168,21 +168,21 @@ class User < ApplicationRecord end def services_enabled - ldap_entry[:service] || [] + ldap_entry[:services_enabled] || [] end def enable_service(service) current_services = services_enabled new_services = Array(service).map(&:to_s) services = (current_services + new_services).uniq - ldap.replace_attribute(dn, :service, services) + ldap.replace_attribute(dn, :serviceEnabled, services) end def disable_service(service) current_services = services_enabled disabled_services = Array(service).map(&:to_s) services = (current_services - disabled_services).uniq - ldap.replace_attribute(dn, :service, services) + ldap.replace_attribute(dn, :serviceEnabled, services) end def disable_all_services diff --git a/app/services/ldap_service.rb b/app/services/ldap_service.rb index 91b6dd9..371f9a9 100644 --- a/app/services/ldap_service.rb +++ b/app/services/ldap_service.rb @@ -74,9 +74,10 @@ class LdapService < ApplicationService mail: e.try(:mail) ? e.mail.first : nil, display_name: e.try(:displayName) ? e.displayName.first : nil, admin: e.try(:admin) ? 'admin' : nil, - service: e.try(:service), + services_enabled: e.try(:serviceEnabled), email_maildrop: e.try(:mailRoutingAddress), - email_password: e.try(:mailpassword) + email_password: e.try(:mailpassword), + nostr_key: e.try(:nostrKey) } end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 7011793..3758d3b 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -66,7 +66,7 @@ RSpec.describe User, type: :model do it "returns the entries from the LDAP service attribute" do expect(user).to receive(:ldap_entry).and_return({ 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"]) end @@ -76,21 +76,21 @@ RSpec.describe User, type: :model do before do allow(user).to receive(:ldap_entry).and_return({ 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) end it "adds the service to the LDAP entry" do 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) end it "adds multiple service to the LDAP entry" do 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]) end @@ -100,21 +100,21 @@ RSpec.describe User, type: :model do before do allow(user).to receive(:ldap_entry).and_return({ 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) end it "removes the service from the LDAP entry" do 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) end it "removes multiple services from the LDAP entry" do 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"]) end From 2c9ecc1fefc270a8104ee6014221d7c561be5c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sat, 16 Mar 2024 16:03:00 +0100 Subject: [PATCH 6/8] Add nostr icons --- app/views/icons/_nostrich-head.html.erb | 6 ++++++ app/views/icons/_nostrich-n.html.erb | 3 +++ app/views/icons/_nostrich.html.erb | 3 +++ 3 files changed, 12 insertions(+) create mode 100644 app/views/icons/_nostrich-head.html.erb create mode 100644 app/views/icons/_nostrich-n.html.erb create mode 100644 app/views/icons/_nostrich.html.erb diff --git a/app/views/icons/_nostrich-head.html.erb b/app/views/icons/_nostrich-head.html.erb new file mode 100644 index 0000000..523f1a1 --- /dev/null +++ b/app/views/icons/_nostrich-head.html.erb @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/views/icons/_nostrich-n.html.erb b/app/views/icons/_nostrich-n.html.erb new file mode 100644 index 0000000..84cc4e3 --- /dev/null +++ b/app/views/icons/_nostrich-n.html.erb @@ -0,0 +1,3 @@ + + + diff --git a/app/views/icons/_nostrich.html.erb b/app/views/icons/_nostrich.html.erb new file mode 100644 index 0000000..0567156 --- /dev/null +++ b/app/views/icons/_nostrich.html.erb @@ -0,0 +1,3 @@ + + + From 3715cb518be8ddb4eebc8297dc79b73da5c36c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sat, 16 Mar 2024 16:03:15 +0100 Subject: [PATCH 7/8] User Settings: Rename Experiments to Nostr And use a nostr icon --- app/controllers/settings_controller.rb | 8 ++++---- .../settings/{_experiments.html.erb => _nostr.html.erb} | 0 app/views/shared/_sidenav_settings.html.erb | 4 ++-- .../settings/{experiments_spec.rb => nostr_spec.rb} | 6 +++--- spec/requests/settings_spec.rb | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) rename app/views/settings/{_experiments.html.erb => _nostr.html.erb} (100%) rename spec/features/settings/{experiments_spec.rb => nostr_spec.rb} (89%) diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index b054740..841d0f2 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -12,7 +12,7 @@ class SettingsController < ApplicationController end def show - if @settings_section == "experiments" + if @settings_section == "nostr" session[:shared_secret] ||= SecureRandom.base64(12) end end @@ -120,7 +120,7 @@ class SettingsController < ApplicationController def remove_nostr_pubkey current_user.update! nostr_pubkey: nil - redirect_to setting_path(:experiments), flash: { + redirect_to setting_path(:nostr), flash: { success: 'Public key removed from account' } end @@ -134,8 +134,8 @@ class SettingsController < ApplicationController def set_settings_section @settings_section = params[:section] allowed_sections = [ - :profile, :account, :xmpp, :email, :lightning, :remotestorage, - :experiments + :profile, :account, :xmpp, :email, + :lightning, :remotestorage, :nostr ] unless allowed_sections.include?(@settings_section.to_sym) diff --git a/app/views/settings/_experiments.html.erb b/app/views/settings/_nostr.html.erb similarity index 100% rename from app/views/settings/_experiments.html.erb rename to app/views/settings/_nostr.html.erb diff --git a/app/views/shared/_sidenav_settings.html.erb b/app/views/shared/_sidenav_settings.html.erb index 86947b1..c8f17a5 100644 --- a/app/views/shared/_sidenav_settings.html.erb +++ b/app/views/shared/_sidenav_settings.html.erb @@ -34,7 +34,7 @@ <% end %> <% if Setting.nostr_enabled %> <%= render SidenavLinkComponent.new( - name: "Experiments", path: setting_path(:experiments), icon: "science", - active: @settings_section.to_s == "experiments" + name: "Nostr", path: setting_path(:nostr), icon: "nostrich-head", + active: @settings_section.to_s == "nostr" ) %> <% end %> diff --git a/spec/features/settings/experiments_spec.rb b/spec/features/settings/nostr_spec.rb similarity index 89% rename from spec/features/settings/experiments_spec.rb rename to spec/features/settings/nostr_spec.rb index dd43303..c2178fc 100644 --- a/spec/features/settings/experiments_spec.rb +++ b/spec/features/settings/nostr_spec.rb @@ -1,6 +1,6 @@ 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' } before do @@ -9,7 +9,7 @@ RSpec.describe 'Experimental Settings', type: :feature do describe 'Adding a nostr pubkey' 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_css('button[data-settings--nostr-pubkey-target=setPubkey]:disabled') end @@ -26,7 +26,7 @@ RSpec.describe 'Experimental Settings', type: :feature do end scenario 'Remove nostr pubkey from account' do - visit setting_path(:experiments) + visit setting_path(:nostr) expect(page).to have_field("nostr_public_key", with: "npub1qlsc3g0lsl8pw8230w8d9wm6xxcax3f6pkemz5measrmwfxjxteslf2hac", disabled: true) diff --git a/spec/requests/settings_spec.rb b/spec/requests/settings_spec.rb index 8bd04ab..5d2ba17 100644 --- a/spec/requests/settings_spec.rb +++ b/spec/requests/settings_spec.rb @@ -7,9 +7,9 @@ RSpec.describe "Settings", type: :request do login_as user, :scope => :user end - describe "GET /settings/experiments" do + describe "GET /settings/nostr" do it "works" do - get setting_path(:experiments) + get setting_path(:nostr) expect(response).to have_http_status(200) end end From 5a3adba60391ae16321941f0c73f28c33b12f59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sun, 17 Mar 2024 11:04:11 +0100 Subject: [PATCH 8/8] Move nostr pubkeys to LDAP attribute closes #173 --- app/controllers/settings_controller.rb | 17 ++- app/models/user.rb | 16 +-- .../ldap_manager/fetch_user_by_nostr_key.rb | 18 +++ app/services/ldap_manager/update_nostr_key.rb | 16 +++ app/services/ldap_manager_service.rb | 3 - app/services/ldap_service.rb | 28 ++--- ...16153558_remove_nostr_pubkey_from_users.rb | 5 + db/schema.rb | 3 +- spec/features/settings/nostr_spec.rb | 17 ++- spec/models/user_spec.rb | 14 ++- spec/requests/settings_spec.rb | 115 +++++++++++++----- spec/requests/well_known_spec.rb | 13 +- 12 files changed, 188 insertions(+), 77 deletions(-) create mode 100644 app/services/ldap_manager/fetch_user_by_nostr_key.rb create mode 100644 app/services/ldap_manager/update_nostr_key.rb create mode 100644 db/migrate/20240316153558_remove_nostr_pubkey_from_users.rb diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index 841d0f2..b736c7f 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -88,6 +88,7 @@ class SettingsController < ApplicationController def set_nostr_pubkey signed_event = nostr_event_params[:signed_event].to_h.symbolize_keys + is_valid_id = NostrManager::ValidateId.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]})" @@ -97,28 +98,24 @@ class SettingsController < ApplicationController http_status :unprocessable_entity and return end - pubkey_taken = User.all_except(current_user).where( - ou: current_user.ou, nostr_pubkey: signed_event[:pubkey] - ).any? + user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event[:pubkey]) - if pubkey_taken + if user_with_pubkey.present? && (user_with_pubkey != current_user) flash[:alert] = "Public key already in use for a different account" http_status :unprocessable_entity and return end - current_user.update! nostr_pubkey: signed_event[:pubkey] + LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event[:pubkey]) session[:shared_secret] = nil flash[:success] = "Public key verification successful" http_status :ok - rescue - flash[:alert] = "Public key could not be verified" - http_status :unprocessable_entity and return end # DELETE /settings/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(:nostr), flash: { success: 'Public key removed from account' @@ -165,7 +162,7 @@ class SettingsController < ApplicationController def nostr_event_params params.permit(signed_event: [ - :id, :pubkey, :created_at, :kind, :tags, :content, :sig + :id, :pubkey, :created_at, :kind, :content, :sig, tags: [] ]) end diff --git a/app/models/user.rb b/app/models/user.rb index fb8d95a..f9bd0ae 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -50,8 +50,6 @@ class User < ApplicationRecord validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true, if: -> { defined?(@display_name) } - validates_uniqueness_of :nostr_pubkey, allow_blank: true - validate :acceptable_avatar # @@ -163,6 +161,15 @@ class User < ApplicationRecord @display_name ||= ldap_entry[:display_name] 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 @avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn) end @@ -189,11 +196,6 @@ class User < ApplicationRecord ldap.delete_attribute(dn,:service) end - def nostr_pubkey_bech32 - return nil unless nostr_pubkey.present? - Nostr::PublicKey.new(nostr_pubkey).to_bech32 - end - private def ldap diff --git a/app/services/ldap_manager/fetch_user_by_nostr_key.rb b/app/services/ldap_manager/fetch_user_by_nostr_key.rb new file mode 100644 index 0000000..a808a1c --- /dev/null +++ b/app/services/ldap_manager/fetch_user_by_nostr_key.rb @@ -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 diff --git a/app/services/ldap_manager/update_nostr_key.rb b/app/services/ldap_manager/update_nostr_key.rb new file mode 100644 index 0000000..0e0b8a9 --- /dev/null +++ b/app/services/ldap_manager/update_nostr_key.rb @@ -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 diff --git a/app/services/ldap_manager_service.rb b/app/services/ldap_manager_service.rb index c7a2599..0f43e32 100644 --- a/app/services/ldap_manager_service.rb +++ b/app/services/ldap_manager_service.rb @@ -1,5 +1,2 @@ class LdapManagerService < LdapService - def suffix - @suffix ||= ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org" - end end diff --git a/app/services/ldap_service.rb b/app/services/ldap_service.rb index 371f9a9..8e78616 100644 --- a/app/services/ldap_service.rb +++ b/app/services/ldap_service.rb @@ -1,8 +1,4 @@ class LdapService < ApplicationService - def initialize - @suffix = ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org" - end - def modify(dn, operations=[]) client.modify dn: dn, operations: operations client.get_operation_result.code @@ -45,7 +41,7 @@ class LdapService < ApplicationService end filter = Net::LDAP::Filter.eq("objectClass", objectclass) - entries = 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.each do |e| @@ -55,14 +51,14 @@ class LdapService < ApplicationService def fetch_users(args={}) if args[:ou] - treebase = "ou=#{args[:ou]},cn=users,#{@suffix}" + treebase = "ou=#{args[:ou]},cn=users,#{ldap_suffix}" else treebase = ldap_config["base"] end attributes = %w[ dn cn uid mail displayName admin service - mailRoutingAddress mailpassword + mailRoutingAddress mailpassword nostrKey ] filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*") @@ -77,7 +73,7 @@ class LdapService < ApplicationService services_enabled: e.try(:serviceEnabled), email_maildrop: e.try(:mailRoutingAddress), email_password: e.try(:mailpassword), - nostr_key: e.try(:nostrKey) + nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil } end end @@ -86,7 +82,7 @@ class LdapService < ApplicationService attributes = %w{dn ou description} filter = Net::LDAP::Filter.eq("objectClass", "organizationalUnit") # filter = Net::LDAP::Filter.eq("objectClass", "*") - treebase = "cn=users,#{@suffix}" + treebase = "cn=users,#{ldap_suffix}" entries = client.search(base: treebase, filter: filter, attributes: attributes) @@ -102,10 +98,10 @@ class LdapService < ApplicationService end def add_organization(ou, description, interactive=false) - dn = "ou=#{ou},cn=users,#{@suffix}" + dn = "ou=#{ou},cn=users,#{ldap_suffix}" 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 attrs = { @@ -126,14 +122,14 @@ class LdapService < ApplicationService delete_all_entries! 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 - add_entry @suffix, { + add_entry ldap_suffix, { dc: "kosmos", objectClass: ["top", "domain"], aci: user_read_aci }, true - add_entry "cn=users,#{@suffix}", { + add_entry "cn=users,#{ldap_suffix}", { cn: "users", objectClass: ["top", "organizationalRole"] }, true end @@ -155,4 +151,8 @@ class LdapService < ApplicationService def ldap_config ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env] end + + def ldap_suffix + @ldap_suffix ||= ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org" + end end diff --git a/db/migrate/20240316153558_remove_nostr_pubkey_from_users.rb b/db/migrate/20240316153558_remove_nostr_pubkey_from_users.rb new file mode 100644 index 0000000..bed06f3 --- /dev/null +++ b/db/migrate/20240316153558_remove_nostr_pubkey_from_users.rb @@ -0,0 +1,5 @@ +class RemoveNostrPubkeyFromUsers < ActiveRecord::Migration[7.1] + def change + remove_column :users, :nostr_pubkey, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 934c5f3..0810c7c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # 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| t.string "name", 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.text "ln_password_ciphertext" t.string "ln_account" - t.string "nostr_pubkey" t.datetime "remember_created_at" t.string "remember_token" t.text "preferences" diff --git a/spec/features/settings/nostr_spec.rb b/spec/features/settings/nostr_spec.rb index c2178fc..3de7e12 100644 --- a/spec/features/settings/nostr_spec.rb +++ b/spec/features/settings/nostr_spec.rb @@ -5,6 +5,10 @@ RSpec.describe 'Nostr Settings', type: :feature do before do 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 describe 'Adding a nostr pubkey' do @@ -22,19 +26,22 @@ RSpec.describe 'Nostr Settings', type: :feature do context "With pubkey configured" do before do - user.update! nostr_pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" + allow_any_instance_of(User).to receive(:nostr_pubkey) + .and_return("ce273cbfb0d4e3e06930773a337c1459e4849efd4cb4c751b906a561c98a6d09") end scenario 'Remove nostr pubkey from account' do visit setting_path(:nostr) expect(page).to have_field("nostr_public_key", - with: "npub1qlsc3g0lsl8pw8230w8d9wm6xxcax3f6pkemz5measrmwfxjxteslf2hac", + with: "npub1ecnne0as6n37q6fswuarxlq5t8jgf8hafj6vw5deq6jkrjv2d5ysnehu73", 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" - 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 diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3758d3b..249ac61 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -206,9 +206,21 @@ RSpec.describe User, type: :model do 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 before do - user.update! nostr_pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" + allow_any_instance_of(User).to receive(:ldap_entry) + .and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" }) end it "encodes the hexadecimal pubkey to bech32" do diff --git a/spec/requests/settings_spec.rb b/spec/requests/settings_spec.rb index 5d2ba17..76c1bca 100644 --- a/spec/requests/settings_spec.rb +++ b/spec/requests/settings_spec.rb @@ -2,9 +2,18 @@ require 'rails_helper' RSpec.describe "Settings", type: :request do 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 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 describe "GET /settings/nostr" do @@ -22,6 +31,11 @@ RSpec.describe "Settings", type: :request do context "With valid data" 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: { signed_event: { id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3", @@ -41,41 +55,17 @@ RSpec.describe "Settings", type: :request do expect(response).to have_http_status(200) end - it "saves the pubkey" do - expect(user.nostr_pubkey).to eq("07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3") + it "informs the user about the success" do + expect(flash[:success]).to eq("Public key verification successful") end end - context "With wrong username" do + context "With key already in use by someone else" do before do - 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 "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) + expect(LdapManager::FetchUserByNostrKey).to receive(:call).with( + pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" + ).and_return(other_user) + expect(LdapManager::UpdateNostrKey).not_to receive(:call) post set_nostr_pubkey_settings_path, params: { signed_event: { @@ -96,8 +86,67 @@ RSpec.describe "Settings", type: :request do expect(response).to have_http_status(422) end - it "does not save the pubkey" do - expect(user.nostr_pubkey).to be_nil + it "informs the user about the failure" do + 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 diff --git a/spec/requests/well_known_spec.rb b/spec/requests/well_known_spec.rb index 1a24a12..9862b79 100644 --- a/spec/requests/well_known_spec.rb +++ b/spec/requests/well_known_spec.rb @@ -19,6 +19,11 @@ RSpec.describe "Well-known URLs", type: :request do context "user does not have a nostr pubkey configured" do 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 get "/.well-known/nostr.json?name=spongebob" expect(response).to have_http_status(:not_found) @@ -26,8 +31,12 @@ RSpec.describe "Well-known URLs", type: :request do end context "user with nostr pubkey" do - let(:user) { create :user, cn: 'bobdylan', ou: 'kosmos.org', nostr_pubkey: '438d35a6750d0dd6b75d032af8a768aad76b62f0c70ecb45f9c4d9e63540f7f4' } - before { user.save! } + let(:user) { create :user, cn: 'bobdylan', ou: 'kosmos.org' } + before do + user.save! + allow_any_instance_of(User).to receive(:nostr_pubkey) + .and_return('438d35a6750d0dd6b75d032af8a768aad76b62f0c70ecb45f9c4d9e63540f7f4') + end it "returns a NIP-05 response" do get "/.well-known/nostr.json?name=bobdylan"