cognito_rails 1.0.0 → 1.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f8bbd85c82670198044b07ac6bd693920201e3911166f1432365bd834e54fafd
4
- data.tar.gz: 1c435514395d116f142fb22121bb0bdb7cdb91b46897b48566453e3748ae184d
3
+ metadata.gz: 2d8bc1e0dc872a16d7406a72e5028da49b62c80c5297d6c4428ea45efd5293a1
4
+ data.tar.gz: 71d87358a0e584eba16ada273a582caa7e560b84ba58b06d6b5752b7d1769f9e
5
5
  SHA512:
6
- metadata.gz: 11bea4faacf98021ee61a6ae2280e4da743fd1e62e68c4dc88681c98f735973ccabc5806261df3e1d22243da42eef7fb3ddc2fc02f5c06677e6291f1e3d9f780
7
- data.tar.gz: 26ba725b881ab2333e8ea5823ac199fc4fd430426b85f1f50832d2e9b1985db692615d2543ef7cea6d6a21aa8cddfdc9358194bf0ce30655acb35b0a971a1d90
6
+ metadata.gz: c22e85bfa4b48b5f5614314ae5cf2f696342fdf3ca204ca92a9d23107a1097da897dcd23a67d7635f1a2cd83b8ecaacbf4bdf20bfeba785fb0c9a1328fb671e9
7
+ data.tar.gz: def01df4152f2fa0e0fc3991e1438d5898af2d3485be0824f0d59e83987d65d27268897e90308e353fdc04e7b8658ff88f0e54fd92dad41d263292fed80756d9
@@ -19,7 +19,7 @@ module CognitoRails
19
19
  # @!attribute default_user_class [w]
20
20
  # @return [String,nil]
21
21
  attr_writer :aws_client_credentials, :skip_model_hooks, :aws_region,
22
- :aws_user_pool_id, :default_user_class
22
+ :aws_user_pool_id, :default_user_class, :password_generator
23
23
 
24
24
  # @return [Boolean] skip model hooks
25
25
  def skip_model_hooks
@@ -49,6 +49,10 @@ module CognitoRails
49
49
  def default_user_class
50
50
  @default_user_class || (raise 'Missing config default_user_class')
51
51
  end
52
+
53
+ def password_generator
54
+ @password_generator || CognitoRails::PasswordGenerator.method(:generate)
55
+ end
52
56
  end
53
57
  end
54
58
  end
@@ -28,10 +28,13 @@ module CognitoRails
28
28
  # @return [Array<ActiveRecord::Base>] all users
29
29
  # @raise [CognitoRails::Error] if failed to fetch users
30
30
  # @raise [ActiveRecord::RecordInvalid] if failed to save user
31
+ # @yield [user, user_data] yields user and user_data just before saving
31
32
  def sync_from_cognito!
32
33
  response = User.all
33
34
  response.users.map do |user_data|
34
- sync_user!(user_data)
35
+ sync_user!(user_data) do |user|
36
+ yield user, user_data if block_given?
37
+ end
35
38
  end
36
39
  end
37
40
 
@@ -56,6 +59,8 @@ module CognitoRails
56
59
  user.phone = User.extract_cognito_attribute(user_data.attributes, :phone_number) if user.respond_to?(:phone)
57
60
  _cognito_resolve_custom_attribute(user, user_data)
58
61
 
62
+ yield user if block_given?
63
+
59
64
  user.save!
60
65
  user
61
66
  end
@@ -92,12 +97,17 @@ module CognitoRails
92
97
  def init_cognito_user
93
98
  return if cognito_external_id.present?
94
99
 
100
+ cognito_user = User.new(init_attributes)
101
+ cognito_user.save!
102
+ self.cognito_external_id = cognito_user.id
103
+ end
104
+
105
+ def init_attributes
95
106
  attrs = { email: email, user_class: self.class }
96
107
  attrs[:phone] = phone if respond_to?(:phone)
108
+ attrs[:password] = password if respond_to?(:password)
97
109
  attrs[:custom_attributes] = instance_custom_attributes
98
- cognito_user = User.new(attrs)
99
- cognito_user.save!
100
- self.cognito_external_id = cognito_user.id
110
+ attrs
101
111
  end
102
112
 
103
113
  # @return [Array<Hash>]
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CognitoRails
4
+ class PasswordGenerator
5
+ NUMERIC = (0..9).to_a.freeze
6
+ LOWER_CASE = ('a'..'z').to_a.freeze
7
+ UPPER_CASE = ('A'..'Z').to_a.freeze
8
+ SPECIAL = [
9
+ '^', '$', '*', '.', '[', ']', '{', '}',
10
+ '(', ')', '?', '"', '!', '@', '#', '%',
11
+ '&', '/', '\\', ',', '>', '<', "'", ':',
12
+ ';', '|', '_', '~', '`', '=', '+', '-'
13
+ ].freeze
14
+
15
+ # Generates a random password given a length range
16
+ #
17
+ # @param range [Range]
18
+ # @return [String]
19
+ def self.generate(range = 8..16)
20
+ password_length = rand(range)
21
+ numeric_count = rand(1..(password_length-3))
22
+
23
+ lower_case_count = rand(1..(password_length-(numeric_count+2)))
24
+ upper_case_count = rand(1..(password_length-(numeric_count + lower_case_count + 1)))
25
+ special_count = password_length-(numeric_count + lower_case_count + upper_case_count)
26
+
27
+ numeric_characters = numeric_count.times.map { NUMERIC.sample }
28
+ lower_case_characters = lower_case_count.times.map { LOWER_CASE.sample }
29
+ upper_case_characters = upper_case_count.times.map { UPPER_CASE.sample }
30
+ special_characters = special_count.times.map { SPECIAL.sample }
31
+
32
+ (numeric_characters + lower_case_characters + upper_case_characters + special_characters).shuffle.join
33
+ end
34
+ end
35
+ end
@@ -39,7 +39,7 @@ module CognitoRails
39
39
  def initialize(attributes = {})
40
40
  attributes = attributes.with_indifferent_access
41
41
  self.email = attributes[:email]
42
- self.password = SecureRandom.urlsafe_base64 || attributes[:password]
42
+ self.password = attributes[:password] || Config.password_generator.call
43
43
  self.phone = attributes[:phone]
44
44
  self.user_class = attributes[:user_class] || Config.default_user_class.constantize
45
45
  self.custom_attributes = attributes[:custom_attributes]
@@ -2,5 +2,5 @@
2
2
 
3
3
  module CognitoRails
4
4
  # @return [String] gem version
5
- VERSION = '1.0.0'
5
+ VERSION = '1.2.0'
6
6
  end
data/lib/cognito_rails.rb CHANGED
@@ -15,6 +15,7 @@ module CognitoRails
15
15
  autoload :Model
16
16
  autoload :User
17
17
  autoload :JWT
18
+ autoload :PasswordGenerator
18
19
 
19
20
  # @private
20
21
  module ModelInitializer
@@ -1,8 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- # rubocop:disable Metrics/BlockLength
4
3
  RSpec.describe CognitoRails::Controller, type: :model do
5
- # rubocop:enable Metrics/BlockLength
6
4
  include CognitoRails::Helpers
7
5
 
8
6
  context 'with an API controller' do
@@ -2,9 +2,7 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- # rubocop:disable Metrics/BlockLength
6
5
  RSpec.describe CognitoRails::JWT, type: :model do
7
- # rubocop:enable Metrics/BlockLength
8
6
  before do
9
7
  allow(URI).to receive(:open).and_return(double(read: jwks))
10
8
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe CognitoRails::PasswordGenerator do
6
+ it 'generates a password' do
7
+ expect(described_class.generate).to be_a(String)
8
+ end
9
+
10
+ it 'generates a password with the correct length' do
11
+ 1000.times do
12
+ expect(described_class.generate(8..8).length).to eq(8)
13
+ end
14
+ end
15
+
16
+ it 'contains at least one letter, one number, one upper case letter, one symbol' do
17
+ 1000.times do
18
+ password = described_class.generate
19
+ expect(password).to match(/[a-z]/)
20
+ expect(password).to match(/[A-Z]/)
21
+ expect(password).to match(/[0-9]/)
22
+ include_symbol = CognitoRails::PasswordGenerator::SPECIAL.any? do |symbol|
23
+ password.include?(symbol)
24
+ end
25
+ expect(include_symbol).to be_truthy
26
+ end
27
+ end
28
+ end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'spec_helper'
4
4
 
5
- # rubocop:disable Metrics/BlockLength
6
5
  RSpec.describe CognitoRails::User, type: :model do
7
6
  include CognitoRails::Helpers
8
7
 
@@ -83,6 +82,34 @@ RSpec.describe CognitoRails::User, type: :model do
83
82
  user.destroy!
84
83
  end
85
84
 
85
+ it 'uses the password generator defined in config' do
86
+ CognitoRails::Config.password_generator = -> { 'ciao' }
87
+ expect(CognitoRails::User).to receive(:cognito_client).at_least(:once).and_return(fake_cognito_client)
88
+
89
+ expect(fake_cognito_client).to receive(:admin_create_user).with(
90
+ hash_including(
91
+ temporary_password: 'ciao'
92
+ )
93
+ )
94
+ user = User.new(email: sample_cognito_email)
95
+ user.save!
96
+ ensure
97
+ CognitoRails::Config.password_generator = nil
98
+ end
99
+
100
+ it 'uses the custom password passed as parameter' do
101
+ expect(CognitoRails::User).to receive(:cognito_client).at_least(:once).and_return(fake_cognito_client)
102
+
103
+ expect(fake_cognito_client).to receive(:admin_create_user).with(
104
+ hash_including(
105
+ temporary_password: '12345678'
106
+ )
107
+ )
108
+ user = User.new(email: sample_cognito_email)
109
+ user.password = '12345678'
110
+ user.save!
111
+ end
112
+
86
113
  it 'saves custom attributes in cognito' do
87
114
  expect(CognitoRails::User).to receive(:cognito_client).at_least(:once).and_return(fake_cognito_client)
88
115
 
@@ -116,27 +143,50 @@ RSpec.describe CognitoRails::User, type: :model do
116
143
  expect(CognitoRails::User).to receive(:cognito_client).at_least(:once).and_return(fake_cognito_client)
117
144
  end
118
145
 
119
- it '#sync_from_cognito!' do
120
- expect(fake_cognito_client).to receive(:list_users).and_return(
121
- OpenStruct.new(
122
- users: [
123
- build_cognito_user_data('some@example.com'),
124
- build_cognito_user_data('some2@example.com')
125
- ],
126
- pagination_token: nil
146
+ context '#sync_from_cognito!' do
147
+ before do
148
+ expect(fake_cognito_client).to receive(:list_users).and_return(
149
+ OpenStruct.new(
150
+ users: [
151
+ build_cognito_user_data('some@example.com'),
152
+ build_cognito_user_data('some2@example.com')
153
+ ],
154
+ pagination_token: nil
155
+ )
127
156
  )
128
- )
129
-
130
- expect do
131
- users = User.sync_from_cognito!
132
-
133
- expect(users).to be_a(Array)
134
- expect(users.size).to eq(2)
135
- expect(users.first).to be_a(User)
136
- end.to change { User.count }.by(2)
137
-
138
- expect(User.pluck(:email)).to match_array(['some@example.com', 'some2@example.com'])
139
- expect(User.pluck(:name)).to match_array(['Giovanni', 'Giovanni'])
157
+ end
158
+ it 'imports all users correctly' do
159
+ expect do
160
+ users = User.sync_from_cognito!
161
+
162
+ expect(users).to be_a(Array)
163
+ expect(users.size).to eq(2)
164
+ expect(users.first).to be_a(User)
165
+ end.to change { User.count }.by(2)
166
+
167
+ expect(User.pluck(:email)).to match_array(['some@example.com', 'some2@example.com'])
168
+ expect(User.pluck(:name)).to match_array(['John Doe', 'John Doe'])
169
+ end
170
+
171
+ it 'allows to specify a block with extra changes applied pre-save' do
172
+ expect do
173
+ i = 0
174
+ users = EnrichedUser.sync_from_cognito! do |user, cognito_user|
175
+ i += 1
176
+ name = cognito_user.attributes.find { |a| a.name == 'custom:name' }
177
+ user.first_name = name.value.split(' ').first + i.to_s
178
+ user.last_name = name.value.split(' ').last
179
+ end
180
+
181
+ expect(users).to be_a(Array)
182
+ expect(users.size).to eq(2)
183
+ expect(users.first).to be_a(EnrichedUser)
184
+ end.to change { EnrichedUser.count }.by(2)
185
+
186
+ expect(EnrichedUser.pluck(:email)).to match_array(['some@example.com', 'some2@example.com'])
187
+ expect(EnrichedUser.order(:id).pluck(:first_name)).to match_array(['John1', 'John2'])
188
+ expect(EnrichedUser.order(:id).pluck(:last_name)).to match_array(['Doe', 'Doe'])
189
+ end
140
190
  end
141
191
 
142
192
  it '#sync_to_cognito!' do
@@ -54,7 +54,7 @@ module CognitoRails::Helpers
54
54
  ),
55
55
  OpenStruct.new(
56
56
  name: 'custom:name',
57
- value: 'Giovanni'
57
+ value: 'John Doe'
58
58
  )
59
59
  ],
60
60
  mfa_options: []
@@ -13,8 +13,23 @@ class User < ActiveRecord::Base
13
13
  cognito_verify_email
14
14
  define_cognito_attribute 'role', 'user'
15
15
  define_cognito_attribute 'name', :name
16
+
17
+ attr_accessor :password
18
+ end
19
+
20
+ class EnrichedUser < ActiveRecord::Base
21
+ validates :email, presence: true
22
+ validates :email, uniqueness: true
23
+ validates :first_name, :last_name, presence: true
24
+
25
+ as_cognito_user
26
+ cognito_verify_email
27
+ define_cognito_attribute 'role', 'user'
28
+
29
+ attr_accessor :password
16
30
  end
17
31
 
32
+
18
33
  class Admin < ActiveRecord::Base
19
34
  validates :email, presence: true
20
35
  validates :email, uniqueness: true
@@ -39,6 +54,14 @@ module Schema
39
54
  t.timestamps null: false
40
55
  end
41
56
 
57
+ create_table :enriched_users, force: true do |t|
58
+ t.string "email", null: false
59
+ t.string "first_name", null: false
60
+ t.string "last_name", null: false
61
+ t.string "external_id", null: false
62
+ t.timestamps null: false
63
+ end
64
+
42
65
  create_table :admins, force: true do |t|
43
66
  t.string "email", null: false
44
67
  t.string "phone", null: false
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cognito_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mònade
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-30 00:00:00.000000000 Z
11
+ date: 2023-05-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -97,10 +97,12 @@ files:
97
97
  - lib/cognito_rails/controller.rb
98
98
  - lib/cognito_rails/jwt.rb
99
99
  - lib/cognito_rails/model.rb
100
+ - lib/cognito_rails/password_generator.rb
100
101
  - lib/cognito_rails/user.rb
101
102
  - lib/cognito_rails/version.rb
102
103
  - spec/cognito_rails/controller_spec.rb
103
104
  - spec/cognito_rails/jwt_spec.rb
105
+ - spec/cognito_rails/password_generator_spec.rb
104
106
  - spec/cognito_rails/user_spec.rb
105
107
  - spec/factories/user.rb
106
108
  - spec/spec_helper.rb
@@ -133,6 +135,7 @@ summary: Add Cognito authentication to your Rails API
133
135
  test_files:
134
136
  - spec/cognito_rails/controller_spec.rb
135
137
  - spec/cognito_rails/jwt_spec.rb
138
+ - spec/cognito_rails/password_generator_spec.rb
136
139
  - spec/cognito_rails/user_spec.rb
137
140
  - spec/factories/user.rb
138
141
  - spec/spec_helper.rb