belt 0.3.44 → 0.4.1
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 +4 -4
- data/CHANGELOG.md +111 -0
- data/README.md +40 -0
- data/lib/belt/authentication/claims.rb +124 -0
- data/lib/belt/authentication/cognito_authenticatable/class_methods.rb +129 -0
- data/lib/belt/authentication/cognito_authenticatable.rb +109 -0
- data/lib/belt/authentication/configuration.rb +69 -0
- data/lib/belt/authentication/controller.rb +123 -0
- data/lib/belt/authentication/model_macro.rb +47 -0
- data/lib/belt/authentication.rb +39 -0
- data/lib/belt/cli/auth_command.rb +27 -0
- data/lib/belt/cli/deploy_command.rb +27 -0
- data/lib/belt/cli/dns_command.rb +510 -241
- data/lib/belt/cli/environment_command.rb +5 -5
- data/lib/belt/cli/explain_command.rb +6 -0
- data/lib/belt/cli/frontend_deploy_command.rb +6 -0
- data/lib/belt/cli/frontend_env_command.rb +13 -4
- data/lib/belt/cli/logs_command.rb +11 -0
- data/lib/belt/cli/server_command.rb +5 -0
- data/lib/belt/cli/tables_command.rb +29 -4
- data/lib/belt/configuration.rb +26 -0
- data/lib/belt/docs/authentication.md +186 -0
- data/lib/belt/errors.rb +8 -0
- data/lib/belt/version.rb +1 -1
- data/lib/belt.rb +2 -19
- data/lib/belt_controller/base.rb +4 -0
- data/lib/belt_controller/implicit_response.rb +1 -0
- data/lib/templates/generate/auth/user_model.rb.erb +21 -0
- metadata +11 -1
|
@@ -95,11 +95,11 @@ module Belt
|
|
|
95
95
|
if @parent_environment
|
|
96
96
|
puts "\nThis is a nested environment under '#{@parent_environment}'."
|
|
97
97
|
puts "It will use the parent's wildcard certificate and hosted zone."
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
# Nested envs use an `api-<env>` prefix (not `api.<env>`) so the API
|
|
99
|
+
# host stays a single level under the parent's *.<parent>.<domain>
|
|
100
|
+
# wildcard cert. See lib/templates/module/dns.tf.erb (api_domain).
|
|
101
|
+
puts "Frontend will be: https://#{@env_name}.#{@parent_environment}.#{@domain}"
|
|
102
|
+
puts "API will be: https://api-#{@env_name}.#{@parent_environment}.#{@domain}"
|
|
103
103
|
puts "\nDeploy with:"
|
|
104
104
|
puts " belt deploy #{@env_name}"
|
|
105
105
|
puts "\nNo DNS delegation needed — the parent environment handles DNS."
|
|
@@ -17,6 +17,12 @@ module Belt
|
|
|
17
17
|
'model' => 'models',
|
|
18
18
|
'activeitem' => 'models',
|
|
19
19
|
'dynamodb' => 'models',
|
|
20
|
+
'auth' => 'authentication',
|
|
21
|
+
'cognito' => 'authentication',
|
|
22
|
+
'users' => 'authentication',
|
|
23
|
+
'user' => 'authentication',
|
|
24
|
+
'current_user' => 'authentication',
|
|
25
|
+
'login' => 'authentication',
|
|
20
26
|
'deploy' => 'deployment',
|
|
21
27
|
'deploying' => 'deployment',
|
|
22
28
|
'terraform' => 'deployment',
|
|
@@ -75,6 +75,12 @@ module Belt
|
|
|
75
75
|
|
|
76
76
|
private
|
|
77
77
|
|
|
78
|
+
# Load infrastructure/<env>/belt.rb and apply its aws_profile + env vars
|
|
79
|
+
# to the current process. Without this, `terraform output` can't reach the
|
|
80
|
+
# S3 state backend (403), fetch_tf_output returns nil, and the deploy aborts
|
|
81
|
+
# with a misleading "Could not determine S3 bucket" error. The full
|
|
82
|
+
# `belt deploy` path applies this before invoking the frontend deploy;
|
|
83
|
+
# standalone `belt deploy frontend` must do it too.
|
|
78
84
|
def load_and_apply_env_config!
|
|
79
85
|
env_config = EnvironmentConfig.load(@env, infra_dir: @infra_dir)
|
|
80
86
|
env_config.apply!
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'app_detection'
|
|
4
4
|
require_relative 'env_resolver'
|
|
5
|
+
require_relative 'environment_config'
|
|
5
6
|
require_relative 'frontend_env_map'
|
|
6
7
|
require_relative 'frontend_registry'
|
|
8
|
+
require_relative 'terraform_command'
|
|
7
9
|
|
|
8
10
|
module Belt
|
|
9
11
|
module CLI
|
|
@@ -111,18 +113,19 @@ module Belt
|
|
|
111
113
|
def initialize(env, frontend: nil)
|
|
112
114
|
@env = env
|
|
113
115
|
@app_name = detect_app_name
|
|
114
|
-
@
|
|
116
|
+
@infra_dir = TerraformCommand.find_infrastructure_dir || 'infrastructure'
|
|
117
|
+
@env_dir = File.join(@infra_dir, @env)
|
|
115
118
|
@frontend = frontend || FrontendRegistry.new.resolve!
|
|
116
119
|
end
|
|
117
120
|
|
|
118
121
|
def run
|
|
122
|
+
load_and_apply_env_config!
|
|
123
|
+
|
|
119
124
|
unless Dir.exist?(@frontend.path)
|
|
120
125
|
abort "Error: No #{@frontend.path}/ directory found. Run `belt generate frontend react` first."
|
|
121
126
|
end
|
|
122
127
|
|
|
123
|
-
unless Dir.exist?(@env_dir)
|
|
124
|
-
abort "Error: infrastructure/#{@env} not found. Run `belt generate environment #{@env}` first."
|
|
125
|
-
end
|
|
128
|
+
abort "Error: #{@env_dir} not found. Run `belt generate environment #{@env}` first." unless Dir.exist?(@env_dir)
|
|
126
129
|
|
|
127
130
|
map = FrontendEnvMap.new(@env, env_dir: @env_dir, frontend_path: @frontend.path)
|
|
128
131
|
|
|
@@ -142,6 +145,12 @@ module Belt
|
|
|
142
145
|
puts "✅ Updated #{result[:path]} (#{updated.join(', ')})"
|
|
143
146
|
end
|
|
144
147
|
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def load_and_apply_env_config!
|
|
152
|
+
EnvironmentConfig.load(@env, infra_dir: @infra_dir).apply!
|
|
153
|
+
end
|
|
145
154
|
end
|
|
146
155
|
end
|
|
147
156
|
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'open3'
|
|
5
|
+
require_relative 'environment_config'
|
|
6
|
+
require_relative 'terraform_command'
|
|
5
7
|
|
|
6
8
|
module Belt
|
|
7
9
|
module CLI
|
|
@@ -76,6 +78,8 @@ module Belt
|
|
|
76
78
|
@env ||= detect_environment
|
|
77
79
|
abort 'Error: Cannot determine environment. Pass -e ENV or set BELT_ENV.' unless @env
|
|
78
80
|
|
|
81
|
+
apply_env_config!
|
|
82
|
+
|
|
79
83
|
@app_name = detect_app_name
|
|
80
84
|
abort 'Error: Cannot determine app name.' unless @app_name
|
|
81
85
|
|
|
@@ -88,6 +92,13 @@ module Belt
|
|
|
88
92
|
|
|
89
93
|
private
|
|
90
94
|
|
|
95
|
+
def apply_env_config!
|
|
96
|
+
infra_dir = TerraformCommand.find_infrastructure_dir || find_infra_dir
|
|
97
|
+
env_config = EnvironmentConfig.load(@env, infra_dir: infra_dir)
|
|
98
|
+
env_config.apply!
|
|
99
|
+
puts " 🔑 Using AWS profile: #{env_config.aws_profile}" if env_config.aws_profile?
|
|
100
|
+
end
|
|
101
|
+
|
|
91
102
|
def parse_args(args)
|
|
92
103
|
i = 0
|
|
93
104
|
while i < args.length
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'base64'
|
|
4
4
|
require 'json'
|
|
5
5
|
require_relative 'app_detection'
|
|
6
|
+
require_relative 'environment_config'
|
|
6
7
|
require_relative 'frontend_env_map'
|
|
7
8
|
require_relative 'frontend_registry'
|
|
8
9
|
require_relative 'terraform_command'
|
|
@@ -125,6 +126,9 @@ module Belt
|
|
|
125
126
|
env_name = @deploy_env || ENV.fetch('BELT_ENV', nil) || TerraformCommand.list_environments.first
|
|
126
127
|
return {} unless env_name
|
|
127
128
|
|
|
129
|
+
infra_dir = TerraformCommand.find_infrastructure_dir
|
|
130
|
+
EnvironmentConfig.load(env_name, infra_dir: infra_dir).apply!
|
|
131
|
+
|
|
128
132
|
FrontendEnvMap.new(env_name, frontend_path: @frontend.path).process_env
|
|
129
133
|
rescue StandardError
|
|
130
134
|
# Fall back to legacy api_url detection if map resolution fails
|
|
@@ -274,6 +278,7 @@ module Belt
|
|
|
274
278
|
next unless Dir.exist?(env_dir)
|
|
275
279
|
next unless File.exist?(File.join(env_dir, '.terraform'))
|
|
276
280
|
|
|
281
|
+
EnvironmentConfig.load(env, infra_dir: infra_dir).apply!
|
|
277
282
|
url = read_api_url_from_outputs(env_dir)
|
|
278
283
|
if url
|
|
279
284
|
@deploy_env = env
|
|
@@ -84,6 +84,9 @@ module Belt
|
|
|
84
84
|
# Extract indexes() declaration
|
|
85
85
|
indexes = extract_indexes(content)
|
|
86
86
|
|
|
87
|
+
# cognito_authenticatable installs a GSI without an indexes() call
|
|
88
|
+
indexes += extract_cognito_indexes(content)
|
|
89
|
+
|
|
87
90
|
# Extract belongs_to associations and generate convention indexes
|
|
88
91
|
indexes += extract_belongs_to_indexes(content)
|
|
89
92
|
|
|
@@ -115,15 +118,33 @@ module Belt
|
|
|
115
118
|
indexes
|
|
116
119
|
end
|
|
117
120
|
|
|
121
|
+
# `cognito_authenticatable` installs an EmailIndex GSI without the model ever
|
|
122
|
+
# calling indexes() — see Belt::Authentication::CognitoAuthenticatable. The
|
|
123
|
+
# generator has to know that, or the table would be created without the GSI and
|
|
124
|
+
# the first email lookup would fail in production instead of here.
|
|
125
|
+
def extract_cognito_indexes(content)
|
|
126
|
+
declaration = uncommented(content).match(/^\s*cognito_authenticatable\b(.*)$/)
|
|
127
|
+
return [] unless declaration
|
|
128
|
+
|
|
129
|
+
options = declaration[1].to_s
|
|
130
|
+
return [] if options.match?(/email_index:\s*false/)
|
|
131
|
+
|
|
132
|
+
name = options.match(/email_index:\s*['"]([^'"]+)['"]/)
|
|
133
|
+
[{ name: name ? name[1] : 'EmailIndex', partition_key: 'email', sort_key: nil }]
|
|
134
|
+
end
|
|
135
|
+
|
|
118
136
|
# Extract belongs_to declarations and generate convention-based GSI indexes.
|
|
119
137
|
# belongs_to :conversation → ConversationIndex with partition_key: 'conversationId'
|
|
138
|
+
#
|
|
139
|
+
# `index: false` opts out — the model is saying the reverse lookup is covered some
|
|
140
|
+
# other way (its own indexes() entry, usually, under a different key name).
|
|
141
|
+
# Generating one anyway produces a GSI on an attribute that doesn't exist.
|
|
120
142
|
def extract_belongs_to_indexes(content)
|
|
121
143
|
indexes = []
|
|
122
144
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
association_name = match[0]
|
|
145
|
+
uncommented(content).scan(/belongs_to\s+:(\w+)([^\n]*)/) do |association_name, options|
|
|
146
|
+
next if options.match?(/index:\s*false/)
|
|
147
|
+
|
|
127
148
|
index_name = "#{Belt::Inflector.classify(association_name)}Index"
|
|
128
149
|
partition_key = "#{association_name}Id"
|
|
129
150
|
|
|
@@ -133,6 +154,10 @@ module Belt
|
|
|
133
154
|
indexes
|
|
134
155
|
end
|
|
135
156
|
|
|
157
|
+
def uncommented(content)
|
|
158
|
+
content.lines.reject { |line| line.strip.start_with?('#') }.join
|
|
159
|
+
end
|
|
160
|
+
|
|
136
161
|
def generate_dynamodb_tf(models)
|
|
137
162
|
dest = File.join(MODULE_DIR, 'dynamodb.tf')
|
|
138
163
|
existing_content = File.exist?(dest) ? File.read(dest) : nil
|
data/lib/belt/configuration.rb
CHANGED
|
@@ -5,6 +5,7 @@ module Belt
|
|
|
5
5
|
#
|
|
6
6
|
# Belt.configure do |config|
|
|
7
7
|
# config.default_format = :json # or :html
|
|
8
|
+
# config.authentication.user_class = 'Account'
|
|
8
9
|
# end
|
|
9
10
|
#
|
|
10
11
|
# Note: infrastructure/<env>/belt.rb uses a separate sandboxed DSL for CLI
|
|
@@ -18,6 +19,12 @@ module Belt
|
|
|
18
19
|
|
|
19
20
|
attr_reader :default_format
|
|
20
21
|
|
|
22
|
+
# Cognito identity settings — see Belt::Authentication::Configuration.
|
|
23
|
+
# Resolved lazily so config can be set before the app's models are loaded.
|
|
24
|
+
def authentication
|
|
25
|
+
@authentication ||= Belt::Authentication::Configuration.new
|
|
26
|
+
end
|
|
27
|
+
|
|
21
28
|
def default_format=(value)
|
|
22
29
|
format = value.to_sym
|
|
23
30
|
unless VALID_FORMATS.include?(format)
|
|
@@ -27,4 +34,23 @@ module Belt
|
|
|
27
34
|
@default_format = format
|
|
28
35
|
end
|
|
29
36
|
end
|
|
37
|
+
|
|
38
|
+
# Runtime configuration accessors live here, not in belt.rb, so that a single
|
|
39
|
+
# subsystem can be required on its own (`require 'belt/authentication'`) and still
|
|
40
|
+
# reach Belt.configuration. Test harnesses that shadow BeltController do exactly that.
|
|
41
|
+
class << self
|
|
42
|
+
# Runtime configuration (lambda/config/environment.rb). Separate from the
|
|
43
|
+
# CLI sandboxed DSL in infrastructure/<env>/belt.rb.
|
|
44
|
+
def configuration
|
|
45
|
+
@configuration ||= Configuration.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def configure
|
|
49
|
+
yield configuration
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def reset_configuration!
|
|
53
|
+
@configuration = Configuration.new
|
|
54
|
+
end
|
|
55
|
+
end
|
|
30
56
|
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Authentication
|
|
2
|
+
|
|
3
|
+
Cognito owns authentication — passwords, MFA, hosted signup, groups. Belt owns the
|
|
4
|
+
**record** of the human Cognito authenticated, so your app can answer its own questions
|
|
5
|
+
about that person without re-reading JWT claims in every controller.
|
|
6
|
+
|
|
7
|
+
Declare it on a model and you're done:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class User < ApplicationRecord
|
|
11
|
+
cognito_authenticatable
|
|
12
|
+
end
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That one line supplies:
|
|
16
|
+
|
|
17
|
+
| | |
|
|
18
|
+
|---|---|
|
|
19
|
+
| Primary key | the Cognito `sub` — resolving the caller is one `GetItem` |
|
|
20
|
+
| Attributes | `email`, `name`, `role`, `email_verified`, `last_seen_on` |
|
|
21
|
+
| GSI | `EmailIndex`, for finding a person by address |
|
|
22
|
+
| Class methods | `.sync_from_claims!`, `.for_sub`, `.for_email` |
|
|
23
|
+
| Instance methods | `#admin?`, `#email_verified?` |
|
|
24
|
+
|
|
25
|
+
Controllers get `current_user`, `authenticate_user!`, `user_signed_in?`, and
|
|
26
|
+
`cognito_admin?` with no `include` and no configuration.
|
|
27
|
+
|
|
28
|
+
## The table
|
|
29
|
+
|
|
30
|
+
```hcl
|
|
31
|
+
resource "aws_dynamodb_table" "users" {
|
|
32
|
+
name = "${var.app_name}-${var.environment}-users"
|
|
33
|
+
billing_mode = "PAY_PER_REQUEST"
|
|
34
|
+
hash_key = "id" # the Cognito sub
|
|
35
|
+
|
|
36
|
+
attribute {
|
|
37
|
+
name = "id"
|
|
38
|
+
type = "S"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
attribute {
|
|
42
|
+
name = "email"
|
|
43
|
+
type = "S"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
global_secondary_index {
|
|
47
|
+
name = "EmailIndex"
|
|
48
|
+
hash_key = "email"
|
|
49
|
+
projection_type = "ALL"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Identity attributes are stored snake_case (`email_verified`, not `emailVerified`) so
|
|
55
|
+
they read the same in the console, in `dynamodb.tf`, and in a GSI key definition.
|
|
56
|
+
|
|
57
|
+
Grant the table to every Lambda, not per route. Authenticated requests read it *before*
|
|
58
|
+
the action runs, so per-route `tables:` means listing it on every route and 500ing on
|
|
59
|
+
whichever one you missed.
|
|
60
|
+
|
|
61
|
+
## In a controller
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
class ProfilesController < ApplicationController
|
|
65
|
+
before_action :authenticate_user!
|
|
66
|
+
|
|
67
|
+
def show
|
|
68
|
+
@profile = current_user
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`authenticate_user!` raises `Belt::Authentication::NotAuthenticated`, which Belt already
|
|
74
|
+
maps to **401**. It has to raise: `before_action` cannot halt the chain by returning a
|
|
75
|
+
response — return values are discarded and the action runs anyway.
|
|
76
|
+
|
|
77
|
+
| Method | |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `current_user` | the user record, or nil. Memoized per request |
|
|
80
|
+
| `user_signed_in?` | is there a Cognito identity on this request? |
|
|
81
|
+
| `authenticate_user!` | `before_action` guard → 401 |
|
|
82
|
+
| `cognito_admin?` | does the token carry a staff Cognito group? |
|
|
83
|
+
| `cognito_claims` | raw claims, if you really need them |
|
|
84
|
+
| `bearer_token` | the raw `Authorization: Bearer` credential |
|
|
85
|
+
|
|
86
|
+
If your app has a *second* credential scheme — an API key for machine callers, say —
|
|
87
|
+
override `skip_cognito_identity?` to declare that such a request is not a human:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
def skip_cognito_identity?
|
|
91
|
+
agent_request? # `Authorization: Bearer fp_...`
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Usually unnecessary: a credential that isn't a Cognito ID token yields no claims and
|
|
96
|
+
therefore no `current_user`. It matters when a request could be read as both.
|
|
97
|
+
|
|
98
|
+
## Just-in-time provisioning
|
|
99
|
+
|
|
100
|
+
There is no signup endpoint to keep in step with Cognito's hosted UI. The first
|
|
101
|
+
authenticated request from a user writes the row; later requests only write when
|
|
102
|
+
something actually drifted — a name changed in Cognito, a staff group was granted or
|
|
103
|
+
revoked, or it's the first sighting today. Steady state is one `GetItem` and no write.
|
|
104
|
+
|
|
105
|
+
`last_seen_on` is a **date**, not a timestamp, precisely so an active session doesn't
|
|
106
|
+
generate a write per request.
|
|
107
|
+
|
|
108
|
+
To hang your own behaviour off that moment, override the hook:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
class User < ApplicationRecord
|
|
112
|
+
cognito_authenticatable
|
|
113
|
+
|
|
114
|
+
# Runs whenever an identity is resolved from a token.
|
|
115
|
+
def after_cognito_sync
|
|
116
|
+
claim_pending_invitations!
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Platform staff
|
|
122
|
+
|
|
123
|
+
`role` is platform-wide: `member` or `admin`. It mirrors a Cognito group on **every**
|
|
124
|
+
request, so removing someone from the group locks them out on their very next call.
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
current_user.admin? # FeatureParity staff — can see across tenants
|
|
128
|
+
cognito_admin? # the grant: does this token carry the group?
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Per-tenant roles (owner of a project, member of an org) are your domain, not this
|
|
132
|
+
concern's. Model them yourself.
|
|
133
|
+
|
|
134
|
+
Grant staff by hand — Terraform should create the group but not manage its members, so
|
|
135
|
+
that a `terraform apply` can't hand out platform-wide read access:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
aws cognito-idp admin-add-user-to-group \
|
|
139
|
+
--user-pool-id <pool> --username <you> --group-name admins
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Configuration
|
|
143
|
+
|
|
144
|
+
Every setting has a working default. An app whose model is `User` and whose staff group
|
|
145
|
+
is `admins` configures nothing.
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
# lambda/config/environment.rb
|
|
149
|
+
Belt.configure do |config|
|
|
150
|
+
config.authentication.user_class = 'Account' # default: 'User'
|
|
151
|
+
config.authentication.admin_groups = %w[staff] # default: ['admins'], or ADMIN_COGNITO_GROUPS
|
|
152
|
+
config.authentication.issuer = '...' # default: derived from COGNITO_USER_POOL_ID
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Macro options:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
cognito_authenticatable roles: %w[member admin support],
|
|
160
|
+
default_role: 'member',
|
|
161
|
+
email_index: 'PeopleEmailIndex' # or false to skip it
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Two token shapes
|
|
165
|
+
|
|
166
|
+
A route can be authenticated either way, and both are handled:
|
|
167
|
+
|
|
168
|
+
1. **API Gateway Cognito authorizer** — claims arrive pre-verified under
|
|
169
|
+
`requestContext.authorizer.claims`, with every value flattened to a string
|
|
170
|
+
(`"true"`, `"[admins, members]"`).
|
|
171
|
+
2. **A raw `Authorization: Bearer <id token>` header** — the Lambda decodes it.
|
|
172
|
+
|
|
173
|
+
Case 2 is signature-unverified by design: where an authorizer is attached, the gateway
|
|
174
|
+
already checked the signature and an unsigned token never reaches your code. What can
|
|
175
|
+
still be checked cheaply is checked — structure, expiry, issuer, and `token_use` (an
|
|
176
|
+
access token is rejected; it carries no email or name).
|
|
177
|
+
|
|
178
|
+
Anything that isn't a Cognito ID token — including your own API key scheme sharing the
|
|
179
|
+
same header — reads as "no Cognito identity" rather than an error, so
|
|
180
|
+
`current_user` is simply nil.
|
|
181
|
+
|
|
182
|
+
## See also
|
|
183
|
+
|
|
184
|
+
- `belt explain models` — ActiveItem
|
|
185
|
+
- `belt explain controllers` — `before_action`, response helpers
|
|
186
|
+
- `belt generate auth` — create the Cognito user pool
|
data/lib/belt/errors.rb
ADDED
data/lib/belt/version.rb
CHANGED
data/lib/belt.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'activeitem'
|
|
4
4
|
require_relative 'belt/version'
|
|
5
5
|
require_relative 'belt/root'
|
|
6
|
+
require_relative 'belt/errors'
|
|
6
7
|
require_relative 'belt/configuration'
|
|
7
8
|
require_relative 'belt/http_status'
|
|
8
9
|
require_relative 'belt/parameters'
|
|
@@ -11,30 +12,11 @@ require_relative 'belt/lambda_handler'
|
|
|
11
12
|
require_relative 'belt/action_router'
|
|
12
13
|
|
|
13
14
|
module Belt
|
|
14
|
-
class AuthenticationError < StandardError; end
|
|
15
|
-
class RecordNotFound < StandardError; end
|
|
16
|
-
class ActionNotFound < StandardError; end
|
|
17
|
-
class TemplateNotFound < StandardError; end
|
|
18
|
-
|
|
19
15
|
@controller_paths = []
|
|
20
16
|
|
|
21
17
|
class << self
|
|
22
18
|
attr_reader :controller_paths
|
|
23
19
|
|
|
24
|
-
# Runtime configuration (lambda/config/environment.rb). Separate from the
|
|
25
|
-
# CLI sandboxed DSL in infrastructure/<env>/belt.rb.
|
|
26
|
-
def configuration
|
|
27
|
-
@configuration ||= Configuration.new
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def configure
|
|
31
|
-
yield configuration
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def reset_configuration!
|
|
35
|
-
@configuration = Configuration.new
|
|
36
|
-
end
|
|
37
|
-
|
|
38
20
|
# Auto-discover lambda/controllers dirs in all loaded gems
|
|
39
21
|
def gem_controller_paths
|
|
40
22
|
@gem_controller_paths ||= discover_gem_paths('lambda/controllers')
|
|
@@ -72,5 +54,6 @@ module Belt
|
|
|
72
54
|
end
|
|
73
55
|
end
|
|
74
56
|
|
|
57
|
+
require_relative 'belt/authentication'
|
|
75
58
|
require_relative 'belt_controller/base'
|
|
76
59
|
require_relative 'belt/controllers/welcome_controller'
|
data/lib/belt_controller/base.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'cgi'
|
|
5
5
|
require_relative '../belt/parameters'
|
|
6
|
+
require_relative '../belt/authentication'
|
|
6
7
|
require_relative '../belt/helpers/response'
|
|
7
8
|
require_relative '../belt/helpers/error_logging'
|
|
8
9
|
require_relative '../belt/helpers/cors_origin'
|
|
@@ -14,6 +15,9 @@ module BeltController
|
|
|
14
15
|
include Belt::Helpers::Response
|
|
15
16
|
include Belt::Rendering
|
|
16
17
|
include ImplicitResponse
|
|
18
|
+
# Cognito identity: current_user, authenticate_user!, cognito_admin?.
|
|
19
|
+
# Inert unless the app declares `cognito_authenticatable` on a model.
|
|
20
|
+
include Belt::Authentication::Controller
|
|
17
21
|
|
|
18
22
|
attr_reader :event, :body
|
|
19
23
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# <%= @user_class %> — a human who signs into this app. One row per Cognito user.
|
|
4
|
+
#
|
|
5
|
+
# Cognito owns authentication (passwords, MFA, the `admins` group). `cognito_authenticatable`
|
|
6
|
+
# supplies everything derived from a token — the `sub` as primary key, email/name/role,
|
|
7
|
+
# just-in-time provisioning, an EmailIndex GSI. Run `belt explain authentication` for the
|
|
8
|
+
# full list.
|
|
9
|
+
#
|
|
10
|
+
# Add your own domain below: associations, per-tenant roles, whatever this app needs to
|
|
11
|
+
# know about a person that Cognito doesn't.
|
|
12
|
+
class <%= @user_class %> < ApplicationRecord
|
|
13
|
+
cognito_authenticatable
|
|
14
|
+
|
|
15
|
+
# has_many :memberships, foreign_key: 'cognito_sub', index: 'CognitoIndex'
|
|
16
|
+
|
|
17
|
+
# Runs whenever an identity is resolved from a token — first sight and every request
|
|
18
|
+
# after. Override to bind pending invitations, seed a workspace, and so on.
|
|
19
|
+
# def after_cognito_sync
|
|
20
|
+
# end
|
|
21
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: belt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stowzilla
|
|
@@ -102,6 +102,13 @@ files:
|
|
|
102
102
|
- lib/belt/action_router.rb
|
|
103
103
|
- lib/belt/assets/belt-default.jpg
|
|
104
104
|
- lib/belt/assets/welcome.css
|
|
105
|
+
- lib/belt/authentication.rb
|
|
106
|
+
- lib/belt/authentication/claims.rb
|
|
107
|
+
- lib/belt/authentication/cognito_authenticatable.rb
|
|
108
|
+
- lib/belt/authentication/cognito_authenticatable/class_methods.rb
|
|
109
|
+
- lib/belt/authentication/configuration.rb
|
|
110
|
+
- lib/belt/authentication/controller.rb
|
|
111
|
+
- lib/belt/authentication/model_macro.rb
|
|
105
112
|
- lib/belt/cli.rb
|
|
106
113
|
- lib/belt/cli/apex_dns_sync.rb
|
|
107
114
|
- lib/belt/cli/app_detection.rb
|
|
@@ -149,6 +156,7 @@ files:
|
|
|
149
156
|
- lib/belt/cli/zip_artifact_builder.rb
|
|
150
157
|
- lib/belt/configuration.rb
|
|
151
158
|
- lib/belt/controllers/welcome_controller.rb
|
|
159
|
+
- lib/belt/docs/authentication.md
|
|
152
160
|
- lib/belt/docs/backups.md
|
|
153
161
|
- lib/belt/docs/console.md
|
|
154
162
|
- lib/belt/docs/controllers.md
|
|
@@ -161,6 +169,7 @@ files:
|
|
|
161
169
|
- lib/belt/docs/plugins.md
|
|
162
170
|
- lib/belt/docs/routing.md
|
|
163
171
|
- lib/belt/docs/structure.md
|
|
172
|
+
- lib/belt/errors.rb
|
|
164
173
|
- lib/belt/helpers/cors_origin.rb
|
|
165
174
|
- lib/belt/helpers/error_logging.rb
|
|
166
175
|
- lib/belt/helpers/response.rb
|
|
@@ -207,6 +216,7 @@ files:
|
|
|
207
216
|
- lib/templates/generate/auth/frontend/apiClient.js
|
|
208
217
|
- lib/templates/generate/auth/frontend/auth.css
|
|
209
218
|
- lib/templates/generate/auth/frontend/auth.js
|
|
219
|
+
- lib/templates/generate/auth/user_model.rb.erb
|
|
210
220
|
- lib/templates/generate/controller.rb.erb
|
|
211
221
|
- lib/templates/generate/model.rb.erb
|
|
212
222
|
- lib/templates/module/dns.tf.erb
|