ons-ldap 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b2d412ce6348a1a3eefe1ff3bc96a1fac4f980f9
4
+ data.tar.gz: d77ef02808888a498f15a5970861faac220e958e
5
+ SHA512:
6
+ metadata.gz: 811b492711c0b3a4cb97a54717fc2ad486daf1acbc33f65cfc7436a56aad8943063ec9df5d27b6546d119d44b761f9124256115ae75bd14df17f50b9ad1472c2
7
+ data.tar.gz: 43fb9137d557753022d7203e4314fccf2ad1b5aef4a9de4a621f5fce4f360deba45ea2076f72b31e2e3337c1c408b67eebe5bc21965851a7bd17ec4797ff348c
@@ -0,0 +1,38 @@
1
+ # ONS LDAP RubyGem
2
+ Thin wrapper around the [net-ldap](https://rubygems.org/gems/net-ldap) RubyGem. Contains a simple `LDAPConnection` class that can be used to authenticate against an LDAP directory.
3
+
4
+ ## Installation
5
+
6
+ ```
7
+ gem install ons-ldap
8
+ ```
9
+
10
+ ## Example
11
+
12
+ ```ruby
13
+ require 'ons-ldap'
14
+
15
+ host = 'localhost ' # LDAP server host
16
+ port = '398' # LDAP server port
17
+ base = 'dc=example,dc=com' # LDAP tree base
18
+
19
+ # Hash of LDAP group names.
20
+ groups = { admins: 'admins', users: 'users' }
21
+
22
+ ldap_connection = LDAPConnection.new(host, port, base, groups, logger)
23
+ user_entry = ldap_connection.authenticate('johntopley', 'password')
24
+
25
+ user_entry.user_id #=> 'johntopley'
26
+ user_entry.display_name #=> 'John Topley'
27
+ user_entry.token # 2FA token, stored in LDAP's employeeNumber field for expediency
28
+ user_entry.groups #=> ['admins', 'users']
29
+ ```
30
+
31
+ ## Testing
32
+
33
+ ```
34
+ rake test
35
+ ```
36
+
37
+ ## Copyright
38
+ Copyright (C) 2016 Crown Copyright (Office for National Statistics)
@@ -0,0 +1 @@
1
+ require_relative 'ons-ldap/ldap_connection'
@@ -0,0 +1,106 @@
1
+ require 'net/ldap'
2
+
3
+ UserEntry = Struct.new(:user_id, :display_name, :token, :groups)
4
+
5
+ class LDAPConnection
6
+
7
+ # Class instance variables.
8
+ class << self
9
+ attr_accessor :host
10
+ attr_accessor :port
11
+ attr_accessor :base
12
+ attr_accessor :groups
13
+ attr_accessor :logger
14
+ end
15
+
16
+ def initialize(host, port, base, groups, logger)
17
+ self.class.host = host
18
+ self.class.port = port.to_i
19
+ self.class.base = base
20
+ self.class.groups = groups
21
+ self.class.logger = logger
22
+ end
23
+
24
+ def authenticate(username, password)
25
+ user_entry = nil
26
+
27
+ # Have to use the username DN format below for the bind operation to succeed.
28
+ auth = { method: :simple, username: "uid=#{username},ou=Users,#{self.class.base}", password: password }
29
+
30
+ Net::LDAP.open(host: self.class.host, port: self.class.port, base: self.class.base, auth: auth) do |ldap|
31
+ unless ldap.bind
32
+ result = ldap.get_operation_result
33
+ self.class.logger.error "LDAP authentication failed for '#{username}': #{result.message} (#{result.code})"
34
+ return nil
35
+ end
36
+
37
+ self.class.logger.info "LDAP authentication succeeded for '#{username}'"
38
+ user_entry = entry_for(username, ldap) || nil
39
+
40
+ # The user must be a member of at least the "<zone>-users" group for authentication to be considered successful.
41
+ users_group = self.class.groups['users']
42
+ unless group_member?(users_group, username, ldap)
43
+ self.class.logger.error "LDAP authentication failed: '#{username}' is not a member of the '#{users_group}' group"
44
+ return nil
45
+ end
46
+
47
+ user_entry.groups = groups_for(username, ldap)
48
+ end
49
+
50
+ user_entry
51
+ end
52
+
53
+ private
54
+
55
+ # Returns the LDAP directory entry for a user.
56
+ def entry_for(username, ldap)
57
+ filter = Net::LDAP::Filter.construct("uid=#{username}")
58
+ attributes = %w(uid displayName employeeNumber) # employeeNumber is used to store the 2FA token.
59
+ user_entry = nil
60
+
61
+ succeeded = ldap.search(filter: filter, attributes: attributes, return_result: false) do |entry|
62
+ user_entry = UserEntry.new(entry.uid.first, entry.displayName.first, entry.employeeNumber.first)
63
+ end
64
+
65
+ unless succeeded
66
+ result = ldap.get_operation_result
67
+ self.class.logger.error "Error searching the LDAP directory using filter '#{filter}': #{result.message} (#{result.code})"
68
+ end
69
+
70
+ user_entry
71
+ end
72
+
73
+ # Returns the LDAP groups a user belongs to.
74
+ def groups_for(username, ldap)
75
+ filter = Net::LDAP::Filter.construct("(&(objectClass=posixGroup)(memberUid=#{username}))")
76
+ attributes = %w(cn)
77
+ groups = []
78
+ succeeded = ldap.search(filter: filter, attributes: attributes, return_result: false) do |entry|
79
+ groups << entry[:cn].first
80
+ end
81
+ unless succeeded
82
+ result = ldap.get_operation_result
83
+ self.class.logger.error "Error searching the LDAP directory using filter '#{filter}': #{result.message} (#{result.code})"
84
+ end
85
+ groups
86
+ end
87
+
88
+ # Returns whether a user is a member of a group.
89
+ def group_member?(group, username, ldap)
90
+ self.class.logger.info "group: '#{group}'"
91
+ filter = Net::LDAP::Filter.construct("(&(cn=#{group})(memberUid=#{username}))")
92
+ attributes = %w(cn)
93
+ group_cn = nil
94
+
95
+ succeeded = ldap.search(filter: filter, attributes: attributes, return_result: false) do |entry|
96
+ group_cn = entry.cn.first
97
+ end
98
+
99
+ unless succeeded
100
+ result = ldap.get_operation_result
101
+ self.class.logger.error "Error searching the LDAP directory using filter '#{filter}': #{result.message} (#{result.code})"
102
+ end
103
+
104
+ group_cn == group
105
+ end
106
+ end
@@ -0,0 +1,8 @@
1
+ module ONSLDAP
2
+ module Version
3
+ MAJOR = 1
4
+ MINOR = 0
5
+ TINY = 0
6
+ end
7
+ VERSION = [Version::MAJOR, Version::MINOR, Version::TINY].compact * '.'
8
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ons-ldap
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - John Topley
8
+ - Philip Sharland
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-08-25 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: net-ldap
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: '0.11'
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - "~>"
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0.11'
34
+ description: Simple class for authenticating against an LDAP directory.
35
+ email:
36
+ - john.topley@ons.gov.uk
37
+ - philip.sharland@ons.gov.uk
38
+ executables: []
39
+ extensions: []
40
+ extra_rdoc_files: []
41
+ files:
42
+ - README.md
43
+ - lib/ons-ldap.rb
44
+ - lib/ons-ldap/ldap_connection.rb
45
+ - lib/ons-ldap/version.rb
46
+ homepage: https://github.com/ONSdigital/ldap-rubygem
47
+ licenses:
48
+ - Crown Copyright (Office for National Statistics)
49
+ metadata: {}
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubyforge_project:
66
+ rubygems_version: 2.6.6
67
+ signing_key:
68
+ specification_version: 4
69
+ summary: LDAP connection class
70
+ test_files: []