Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement LDAP check integration #94

Merged
merged 24 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ spec/examples.txt

# Ignore other files
/.idea
/spec/fixtures/files/*
/data/*
/app/parsing_files/*
/app/post_prints/*
Expand All @@ -43,3 +42,4 @@ spec/examples.txt
.cache
.bash_history
.envrc
.irb_history
1 change: 1 addition & 0 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ RSpec/AnyInstance:
- 'spec/integration/ai_integration/ldap_integration_spec.rb'
- 'spec/jobs/application_job_spec.rb'
- 'spec/models/post_print_analyzer_spec.rb'
- 'spec/controllers/ldap_check_controller_spec.rb'

# Offense count: 49
# This cop supports unsafe autocorrection (--autocorrect-all).
Expand Down
21 changes: 21 additions & 0 deletions app/controllers/ldap_check_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class LdapCheckController < ApplicationController
def index; end

def create
should_disable = params[:ldap_should_disable] == '1'
data = params[:ldap_check_file]
result = LdapCheck.new.check(data, should_disable)

if result[:error]
flash[:error] = result[:error]
redirect_to ldap_check_path
else
send_data(
result[:output],
filename: 'ldap_check_results.csv',
type: 'text/csv',
disposition: 'attachment'
)
end
end
end
35 changes: 35 additions & 0 deletions app/services/ai_disable_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

require 'httparty'

class AiDisableClient
class ClientError < StandardError; end

include HTTParty
base_uri ENV.fetch('FAMS_WEBSERVICES_BASE_URI', 'https://betawebservices.digitalmeasures.com/login/service/v4')

def initialize(
username = ENV.fetch('FAMS_WEBSERVICES_USERNAME', nil),
password = ENV.fetch('FAMS_WEBSERVICES_PASSWORD', nil)
)
@options = {
basic_auth: {
username:,
password:
},
headers: {
'Accept' => 'text/xml',
'Content-Type' => 'text/xml'
},
timeout: 180
}
end

def user(uid)
self.class.get("/User/USERNAME:#{uid}", @options)
end

def enable_user(uid, value)
self.class.put("/User/USERNAME:#{uid}", @options.merge({ body: "<User enabled=\"#{value}\"/>" }))
end
end
85 changes: 85 additions & 0 deletions app/services/ldap_check.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# frozen_string_literal: true

class LdapCheck
def initialize(disable_client = AiDisableClient.new)
@disable_client = disable_client
end

def check(data, should_disable)
uids = CSV.parse(data.read, headers: true)
.filter_map { |row| row['Username'] }

return { error: 'No usernames were found in the uploaded CSV. Make sure there is a "Usernames" column.' } if uids.empty?

entries = pull_ldap_data(uids)
disabled_uids = find_disabled_users(entries)

if should_disable
uids_to_disable = entries
.filter { |entry| entry['eduPersonPrimaryAffiliation'].first == 'MEMBER' }
.map { |entry| entry['uid'].first }

disabled_uids = disable_users(disabled_uids, uids_to_disable)
end

output = generate_output(entries, disabled_uids)

{ output: }
end

private

def find_disabled_users(entries)
entries.filter_map do |entry|
uid = entry['uid'].first
data = @disable_client.user(uid)['User']
uid if data && (data['enabled'] == 'false')
end
end

def disable_users(disabled_uids, uids)
uids.each do |uid|
next if disabled_uids.include?(uid)

@disable_client.enable_user(uid, false)
disabled_uids.append(uid)
end

disabled_uids
end

def pull_ldap_data(uids)
conn = Net::LDAP.new(
host: ENV.fetch('CENTRAL_LDAP_HOST', 'test-dirapps.aset.psu.edu'),
port: ENV.fetch('CENTRAL_LDAP_PORT', '389')
)

joined_filter = uids.map { |uid| Net::LDAP::Filter.eq('uid', uid) }.reduce(:|)

conn.search(
base: 'dc=psu,dc=edu',
filter: joined_filter
)
end

def generate_output(entries, disabled_uids)
headers = ['Username', 'Name', 'Primary Affiliation', 'Title', 'Department', 'Campus', 'Disabled?']

CSV.generate do |csv|
csv << headers

entries.each do |entry|
uid = entry['uid'].first
csv << [
uid,
entry['displayName'].first,
entry['eduPersonPrimaryAffiliation'].first,
entry['title'].first,
entry['psBusinessArea'].first,
entry['psCampus'].first,
disabled_uids.include?(uid) ? 'yes' : 'no'
]
end
end
end
end
3 changes: 3 additions & 0 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@
<li class="nav-item">
<a class="nav-link" href=<%=post_prints_path%>>Post Prints Analyzer</a>
</li>
<li class="nav-item">
<a class="nav-link" href=<%=ldap_check_path%>>LDAP Check</a>
</li>
</ul>
</div>
</div>
Expand Down
18 changes: 18 additions & 0 deletions app/views/ldap_check/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<div class="container main-content">
<h2>LDAP Check</h2>
<%= form_with url: ldap_check_path, multipart: true, id: 'ldap-check-form' do |form| %>
<p>
<%= label(:ldap_check_file, text = "Usernames to check") %>
<%= form.file_field :ldap_check_file, required: true %>
</p>

<p>
<%= label(:ldap_should_disable, text = "Disable members?") %>
<%= form.check_box :ldap_should_disable %>
</p>

<div class="row">
<%= form.button 'Check LDAP Data', class: 'btn btn-primary mt-3', type: :submit %>
</div>
<% end %>
</div>
12 changes: 12 additions & 0 deletions app/views/main/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,16 @@
</ol>
</ul>
</br>
<h3>LDAP Check</h3>
<p>Pulls relevent user data from central LDAP. Optionally, disables LDAP members in Activity Insight. </p>
<ol>
<li>Upload a CSV with every username (ie abc123) that should be checked. They must be organized in an "Usernames" column.</li>
<li>Select whether or not members should be disabled.</li>
<li>Submit when ready. <b>It may take some time to process depending on the number of usernames.</b></li>
<li>Save ldap_check_results.csv to somewhere on your computer.</li>
</ol>
<p>
<b>Note:</b> the "Disabled?" header displays the user state <i>after</i> the user has been processed.
This means members that where once enabled will be shown as disabled in the result CSV <i>if disabling was requested</i>.
</p>
</div>
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@
get '/destroy', to: 'publication_listings#destroy'
post '/update', to: 'publication_listings#update'
end
get 'ldap_check', to: 'ldap_check#index'
post 'ldap_check', to: 'ldap_check#create'
end
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ services:
FAMS_S3_BUCKET_API_KEY: ${FAMS_S3_BUCKET_API_KEY}
FAMS_LP_SFTP_USERNAME: ${FAMS_LP_SFTP_USERNAME}
FAMS_LP_SFTP_HOST: ${FAMS_LP_SFTP_HOST}
FAMS_WEBSERVICES_BASE_URI: ${FAMS_WEBSERVICES_BASE_URI}
CENTRAL_LDAP_HOST: ${CENTRAL_LDAP_HOST}
CENTRAL_LDAP_PORT: ${CENTRAL_LDAP_PORT}
build:
args:
UID: "${UID:-1001}"
Expand Down
3 changes: 3 additions & 0 deletions spec/fixtures/files/bad_userames.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Bad
foo
bar
1 change: 1 addition & 0 deletions spec/fixtures/files/empty_usernames.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Username
3 changes: 3 additions & 0 deletions spec/fixtures/files/usernames.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Username
test123
test456
84 changes: 84 additions & 0 deletions spec/services/ldap_check_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
require 'rails_helper'

RSpec.describe LdapCheck, type: :service do
let(:should_disable) { true }
let(:ldap_check) { LdapCheck.new(ai_disable_client) }
let(:ai_disable_client) { instance_double(AiDisableClient) }
let(:mock_ldap_entries) do
[
{ 'uid' => ['test123'], 'eduPersonPrimaryAffiliation' => ['MEMBER'], 'displayName' => ['Test User'], 'title' => ['Test Title'], 'psBusinessArea' => ['Test Dept'], 'psCampus' => ['Test Campus'] },
{ 'uid' => ['test456'], 'eduPersonPrimaryAffiliation' => ['NON-MEMBER'], 'displayName' => ['Another User'], 'title' => ['Another Title'], 'psBusinessArea' => ['Another Dept'], 'psCampus' => ['Another Campus'] }
]
end

before do
allow(ldap_check).to receive(:pull_ldap_data).and_return(mock_ldap_entries)
allow(ai_disable_client).to receive(:enable_user).and_return(nil)
allow(ai_disable_client).to receive(:user) do |uid|
{ 'User' => { 'uid' => uid } }
end
end

describe '#check' do
context 'when CSV has valid usernames' do
let(:csv_data) { file_fixture('usernames.csv').open }

it 'disables members when they are enabled' do
allow(ai_disable_client).to receive(:user) do |uid|
{ 'User' => { 'uid' => uid } }
end

result = ldap_check.check(csv_data, should_disable)
expect(result[:output]).to include('Username,Name,Primary Affiliation,Title,Department,Campus,Disabled?')
expect(result[:output]).to include('test123,Test User,MEMBER,Test Title,Test Dept,Test Campus,yes')
expect(result[:output]).to include('test456,Another User,NON-MEMBER,Another Title,Another Dept,Another Campus,no')
end

it 'keeps disabled entries disabled' do
allow(ai_disable_client).to receive(:user) do |uid|
if uid == 'test123'
{ 'User' => { 'uid' => uid, 'enabled' => 'false' } }
else
{ 'User' => { 'uid' => uid } }
end
end

result = ldap_check.check(csv_data, should_disable)
expect(result[:output]).to include('Username,Name,Primary Affiliation,Title,Department,Campus,Disabled?')
expect(result[:output]).to include('test123,Test User,MEMBER,Test Title,Test Dept,Test Campus,yes')
expect(result[:output]).to include('test456,Another User,NON-MEMBER,Another Title,Another Dept,Another Campus,no')
end
end

context 'when CSV has no usernames' do
let(:csv_data) { file_fixture('empty_usernames.csv').open }

it 'returns an error message' do
result = ldap_check.check(csv_data, should_disable)
expect(result[:error]).to be_present
end
end

context 'when CSV has a bad column' do
let(:csv_data) { file_fixture('bad_userames.csv').open }

it 'returns an error message' do
result = ldap_check.check(csv_data, should_disable)
expect(result[:error]).to be_present
end
end

context 'when should_disable is false' do
let(:csv_data) { file_fixture('usernames.csv').open }
let(:should_disable) { false }

it 'does not disable any users and returns output' do
result = ldap_check.check(csv_data, should_disable)
expect(result[:output]).to include('Username,Name,Primary Affiliation,Title,Department,Campus,Disabled?')
expect(result[:output]).to include('test123,Test User,MEMBER,Test Title,Test Dept,Test Campus,no')
expect(result[:output]).to include('test456,Another User,NON-MEMBER,Another Title,Another Dept,Another Campus,no')
expect(ai_disable_client).not_to receive(:enable_user)
end
end
end
end
Empty file removed vendor/.keep
Empty file.
Loading