cognito_rails 1.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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