Add service attribute to LDAP user entry #91
| @ -18,6 +18,8 @@ class Admin::UsersController < Admin::BaseController | ||||
|     if Setting.lndhub_admin_enabled? | ||||
|       @lndhub_user = @user.lndhub_user | ||||
|     end | ||||
| 
 | ||||
|     @services_enabled = @user.services_enabled | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
|  | ||||
							
								
								
									
										17
									
								
								app/controllers/users/confirmations_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/controllers/users/confirmations_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class Users::ConfirmationsController < Devise::ConfirmationsController | ||||
|   # GET /resource/confirmation?confirmation_token=abcdef | ||||
|   def show | ||||
|     self.resource = resource_class.confirm_by_token(params[:confirmation_token]) | ||||
|     yield resource if block_given? | ||||
| 
 | ||||
|     if resource.errors.empty? | ||||
|       set_flash_message!(:success, :confirmed) | ||||
|       resource.devise_after_confirmation | ||||
|       respond_with_navigational(resource){ redirect_to after_confirmation_path_for(resource_name, resource) } | ||||
|     else | ||||
|       respond_with_navigational(resource.errors, status: :unprocessable_entity){ render :new } | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @ -42,9 +42,9 @@ class User < ApplicationRecord | ||||
| 
 | ||||
|   def ldap_before_save | ||||
|     self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first | ||||
| 
 | ||||
|     dn = Devise::LDAP::Adapter.get_ldap_param(self.cn, "dn") | ||||
|     self.ou = dn.split(',').select{|e| e[0..1] == "ou"}.first.delete_prefix("ou=") | ||||
|     self.ou    = dn.split(',') | ||||
|                    .select{|e| e[0..1] == "ou"}.first | ||||
|                    .delete_prefix("ou=") | ||||
| 
 | ||||
|     if self.confirmed_at.blank? && self.confirmation_token.blank? | ||||
|       # User had an account with a trusted email address before akkounts was a thing | ||||
| @ -52,6 +52,10 @@ class User < ApplicationRecord | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def devise_after_confirmation | ||||
|     enable_service %w[discourse gitea wiki xmpp] | ||||
|   end | ||||
| 
 | ||||
|   def reset_password(new_password, new_password_confirmation) | ||||
|     self.password = new_password | ||||
|     self.password_confirmation = new_password_confirmation | ||||
| @ -70,12 +74,6 @@ class User < ApplicationRecord | ||||
|               end | ||||
|   end | ||||
| 
 | ||||
|   def ldap_entry | ||||
|     return @ldap_entry if defined?(@ldap_entry) | ||||
|     ldap = LdapService.new | ||||
|     @ldap_entry = ldap.fetch_users(uid: self.cn, ou: self.ou).first | ||||
|   end | ||||
| 
 | ||||
|   def address | ||||
|     "#{self.cn}@#{self.ou}" | ||||
|   end | ||||
| @ -90,4 +88,42 @@ class User < ApplicationRecord | ||||
|     lndhub.authenticate self | ||||
|     lndhub.addinvoice payload | ||||
|   end | ||||
| 
 | ||||
|   def dn | ||||
|     return @dn if defined?(@dn) | ||||
|     @dn = Devise::LDAP::Adapter.get_dn(self.cn) | ||||
|   end | ||||
| 
 | ||||
|   def ldap_entry | ||||
|     ldap.fetch_users(uid: self.cn, ou: self.ou).first | ||||
|   end | ||||
| 
 | ||||
|   def services_enabled | ||||
|     ldap_entry[:service] || [] | ||||
|   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) | ||||
|   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) | ||||
|   end | ||||
| 
 | ||||
|   def disable_all_services | ||||
|     ldap.delete_attribute(dn,:service) | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|     def ldap | ||||
|       return @ldap_service if defined?(@ldap_service) | ||||
|       @ldap_service = LdapService.new | ||||
|     end | ||||
| end | ||||
|  | ||||
| @ -3,6 +3,18 @@ class LdapService < ApplicationService | ||||
|     @suffix = ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org" | ||||
|   end | ||||
| 
 | ||||
|   def add_attribute(dn, attr, values) | ||||
|     ldap_client.add_attribute dn, attr, values | ||||
|   end | ||||
| 
 | ||||
|   def replace_attribute(dn, attr, values) | ||||
|     ldap_client.replace_attribute dn, attr, values | ||||
|   end | ||||
| 
 | ||||
|   def delete_attribute(dn, attr) | ||||
|     ldap_client.delete_attribute dn, attr | ||||
|   end | ||||
| 
 | ||||
|   def add_entry(dn, attrs, interactive=false) | ||||
|     puts "Adding entry: #{dn}" if interactive | ||||
|     res = ldap_client.add dn: dn, attributes: attrs | ||||
| @ -10,10 +22,6 @@ class LdapService < ApplicationService | ||||
|     res | ||||
|   end | ||||
| 
 | ||||
|   def add_attribute(dn, attr, value) | ||||
|     ldap_client.add_attribute dn, attr, value | ||||
|   end | ||||
| 
 | ||||
|   def delete_entry(dn, interactive=false) | ||||
|     puts "Deleting entry: #{dn}" if interactive | ||||
|     res = ldap_client.delete dn: dn | ||||
| @ -42,18 +50,17 @@ class LdapService < ApplicationService | ||||
|       treebase = ldap_config["base"] | ||||
|     end | ||||
| 
 | ||||
|     attributes = %w{dn cn uid mail admin} | ||||
|     attributes = %w{dn cn uid mail admin service} | ||||
|     filter     = Net::LDAP::Filter.eq("uid", args[:uid] || "*") | ||||
| 
 | ||||
|     entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes) | ||||
|     entries.sort_by! { |e| e.cn[0] } | ||||
| 
 | ||||
|     entries = entries.collect do |e| | ||||
|       { | ||||
|         uid: e.uid.first, | ||||
|         mail: e.try(:mail) ? e.mail.first : nil, | ||||
|         admin: e.try(:admin) ? 'admin' : nil | ||||
|         # password: e.userpassword.first | ||||
|         admin: e.try(:admin) ? 'admin' : nil, | ||||
|         service: e.try(:service) | ||||
|       } | ||||
|     end | ||||
|   end | ||||
| @ -131,5 +138,4 @@ class LdapService < ApplicationService | ||||
|   def ldap_config | ||||
|     ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env] | ||||
|   end | ||||
| 
 | ||||
| end | ||||
|  | ||||
| @ -1,63 +1,97 @@ | ||||
| <%= render HeaderComponent.new(title: "User: #{@user.address}") %> | ||||
| 
 | ||||
| <%= render MainSimpleComponent.new do %> | ||||
|   <div class="mb-12 sm:flex sm:flex-row sm:gap-x-8"> | ||||
|     <section class="sm:flex-1"> | ||||
|       <h3>Account</h3> | ||||
|       <table class="divided"> | ||||
|         <tbody> | ||||
|           <tr> | ||||
|             <th>Created at</th> | ||||
|             <td><%= @user.created_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td> | ||||
|           </tr> | ||||
|           <tr> | ||||
|             <th>Confirmed at</th> | ||||
|             <td> | ||||
|             <% if @user.confirmed_at %> | ||||
|               <%= @user.confirmed_at.strftime("%Y-%m-%d (%H:%M UTC)") %> | ||||
|             <% else %> | ||||
|               <%= badge "pending", :yellow %> | ||||
|             <% end %> | ||||
|             </td> | ||||
|           </tr> | ||||
|           <tr> | ||||
|             <th>Email</th> | ||||
|             <td><%= @user.email %></td> | ||||
|           </tr> | ||||
|           <tr> | ||||
|             <th>Roles</th> | ||||
|             <td><%= @user.is_admin? ? badge("admin", :red) : "—" %></td> | ||||
|           </tr> | ||||
|           <tr> | ||||
|             <th>Invited by</th> | ||||
|             <td> | ||||
|             <% if @user.inviter %> | ||||
|               <%= link_to @user.inviter.address, admin_user_path(@user.inviter.address), class: 'ks-text-link' %> | ||||
|             <% else %>—<% end %> | ||||
|             </td> | ||||
|           </tr> | ||||
|           <tr> | ||||
|             <th>Invitations available</th> | ||||
|             <td> | ||||
|               <%= @user.invitations.count %> | ||||
|             </td> | ||||
|           </tr> | ||||
|           <tr> | ||||
|             <th class="align-top">Invited users</th> | ||||
|             <td class="align-top"> | ||||
|             <% if @user.invitees.length > 0 %> | ||||
|               <ul class="mb-0"> | ||||
|               <% @user.invitees.order(cn: :asc).each do |invitee| %> | ||||
|                 <li class="leading-none mb-2 last:mb-0"><%= link_to invitee.address, admin_user_path(invitee.address), class: 'ks-text-link' %></li> | ||||
|               <% end %> | ||||
|               </ul> | ||||
|             <% else %>—<% end %> | ||||
|             </td> | ||||
|           </tr> | ||||
|         </tbody> | ||||
|       </table> | ||||
|     </section> | ||||
| 
 | ||||
|     <section class="sm:flex-1 sm:pt-0"> | ||||
|       <!-- <h3>Actions</h3> --> | ||||
|     </section> | ||||
|   </div> | ||||
| 
 | ||||
|   <section> | ||||
|     <h3>Account</h3> | ||||
|     <table class="w-1/2 divided"> | ||||
|     <h3>Services</h3> | ||||
|     <table class="sm:w-1/4"> | ||||
|       <tbody> | ||||
|         <tr> | ||||
|           <th>Created at</th> | ||||
|           <td><%= @user.created_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td> | ||||
|           <td>Discourse</td> | ||||
|           <td><%= check_box_tag 'service_discourse', 'enabled', @services_enabled.include?("discourse"), disabled: true %></td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <th>Confirmed at</th> | ||||
|           <td> | ||||
|           <% if @user.confirmed_at %> | ||||
|             <%= @user.confirmed_at.strftime("%Y-%m-%d (%H:%M UTC)") %> | ||||
|           <% else %> | ||||
|             <%= badge "pending", :yellow %> | ||||
|           <% end %> | ||||
|           </td> | ||||
|           <td>Gitea</td> | ||||
|           <td><%= check_box_tag 'service_gitea', 'enabled', @services_enabled.include?("gitea"), disabled: true %></td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <th>Email</th> | ||||
|           <td><%= @user.email %></td> | ||||
|           <td>Mastodon</td> | ||||
|           <td><%= check_box_tag 'service_mastodon', 'enabled', @services_enabled.include?("mastodon"), disabled: true %></td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <th>Roles</th> | ||||
|           <td><%= @user.is_admin? ? badge("admin", :red) : "—" %></td> | ||||
|           <td>Wiki</td> | ||||
|           <td><%= check_box_tag 'service_wiki', 'enabled', @services_enabled.include?("wiki"), disabled: true %></td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <th>Invited by</th> | ||||
|           <td> | ||||
|           <% if @user.inviter %> | ||||
|             <%= link_to @user.inviter.address, admin_user_path(@user.inviter.address), class: 'ks-text-link' %> | ||||
|           <% else %>—<% end %> | ||||
|           </td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <th>Invitations available</th> | ||||
|           <td> | ||||
|             <%= @user.invitations.count %> | ||||
|           </td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|           <th class="align-top">Invited users</th> | ||||
|           <td class="align-top"> | ||||
|           <% if @user.invitees.length > 0 %> | ||||
|             <ul> | ||||
|             <% @user.invitees.order(cn: :asc).each do |invitee| %> | ||||
|               <li class="leading-none mb-2 last:mb-0"><%= link_to invitee.address, admin_user_path(invitee.address), class: 'ks-text-link' %></li> | ||||
|             <% end %> | ||||
|             </ul> | ||||
|           <% else %>—<% end %> | ||||
|           </td> | ||||
|           <td>XMPP</td> | ||||
|           <td><%= check_box_tag 'service_xmpp', 'enabled', @services_enabled.include?("xmpp"), disabled: true %></td> | ||||
|         </tr> | ||||
|       </tbody> | ||||
|     </table> | ||||
|   </section> | ||||
| 
 | ||||
|   <% if Setting.lndhub_admin_enabled? %> | ||||
|   <% if Setting.lndhub_admin_enabled? && @user.confirmed? %> | ||||
|   <section> | ||||
|     <h3>LndHub</h3> | ||||
|     <% if @lndhub_user %> | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| require 'sidekiq/web' | ||||
| 
 | ||||
| Rails.application.routes.draw do | ||||
|   devise_for :users | ||||
|   devise_for :users, :controllers => { :confirmations => "users/confirmations" } | ||||
| 
 | ||||
|   get 'welcome', to: 'welcome#index' | ||||
|   get 'check_your_email', to: 'welcome#check_your_email' | ||||
|  | ||||
| @ -1,7 +1,16 @@ | ||||
| require 'rails_helper' | ||||
| 
 | ||||
| RSpec.describe User, type: :model do | ||||
|   let(:user) { create :user } | ||||
|   let(:user) { create :user, cn: "philipp" } | ||||
|   let(:dn) { "cn=philipp,ou=kosmos.org,cn=users,dc=kosmos,dc=org" } | ||||
| 
 | ||||
|   describe "#address" do | ||||
|     let(:user) { build :user, cn: "jimmy", ou: "kosmos.org" } | ||||
| 
 | ||||
|     it "returns the user address" do | ||||
|       expect(user.address).to eq("jimmy@kosmos.org") | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe "#is_admin?" do | ||||
|     it "returns true when admin flag is set in LDAP" do | ||||
| @ -21,11 +30,75 @@ RSpec.describe User, type: :model do | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe "#address" do | ||||
|     let(:user) { build :user, cn: "jimmy", ou: "kosmos.org" } | ||||
| 
 | ||||
|     it "returns the user address" do | ||||
|       expect(user.address).to eq("jimmy@kosmos.org") | ||||
|   describe "#services_enabled" 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", "gitea", "wiki", "xmpp"] | ||||
|       }) | ||||
|       expect(user.services_enabled).to eq(["discourse", "gitea", "wiki", "xmpp"]) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe "#enable_service" 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"] | ||||
|       }) | ||||
|       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) | ||||
| 
 | ||||
|       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) | ||||
| 
 | ||||
|       user.enable_service([:wiki, :xmpp]) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe "#disable_service" 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"] | ||||
|       }) | ||||
|       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) | ||||
| 
 | ||||
|       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) | ||||
| 
 | ||||
|       user.disable_service([:xmpp, "gitea"]) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe "#disable_all_services" do | ||||
|     before do | ||||
|       allow(user).to receive(:dn).and_return(dn) | ||||
|     end | ||||
| 
 | ||||
|     it "removes all services from the LDAP entry" do | ||||
|       expect_any_instance_of(LdapService).to receive(:delete_attribute) | ||||
|         .with(dn, :service).and_return(true) | ||||
| 
 | ||||
|       user.disable_all_services | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
| end | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user