liberty 0.3.0 → 0.4.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 +4 -4
- data/.qlty/.gitignore +7 -0
- data/.qlty/qlty.toml +81 -0
- data/CHANGELOG.md +25 -0
- data/Gemfile.lock +1 -1
- data/README.md +51 -0
- data/Rakefile +22 -1
- data/lib/liberty/adapters/error_response.rb +30 -0
- data/lib/liberty/adapters/forbidden_response.rb +19 -0
- data/lib/liberty/adapters/request.rb +17 -5
- data/lib/liberty/adapters/response.rb +23 -1
- data/lib/liberty/adapters/unauthenticated_response.rb +29 -0
- data/lib/liberty/endpoint.rb +11 -0
- data/lib/liberty/router.rb +16 -2
- data/lib/liberty/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9450a8fadf0c6726d4249f2b6a62f4918045795f199be8ec8bec29412bbaebc1
|
|
4
|
+
data.tar.gz: 55ea606054dabaeec94add7f6b5fe63a5d38add6eca13dc9f4fc1ef2754ad5c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 17e9cd7824c5b5038daee7fd5d3d441886865dc78562797cd502b8f96f6fd9cd48b2ed92da934265321757a2237e3682092e8653b1b4370260d73b3d9caef253
|
|
7
|
+
data.tar.gz: 7416e1f47f06cb05dd7fbaadc777f71bbac9bc828a5954f4db401c52e42abc88777ee3fd49ad1e24e22e412953e7d2b188b2a6983b153ea36f535f02a5f7d48c
|
data/.qlty/.gitignore
ADDED
data/.qlty/qlty.toml
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# This file was automatically generated by `qlty init`.
|
|
2
|
+
# You can modify it to suit your needs.
|
|
3
|
+
# We recommend you to commit this file to your repository.
|
|
4
|
+
#
|
|
5
|
+
# This configuration is used by both Qlty CLI and Qlty Cloud.
|
|
6
|
+
#
|
|
7
|
+
# Qlty CLI -- Code quality toolkit for developers
|
|
8
|
+
# Qlty Cloud -- Fully automated Code Health Platform
|
|
9
|
+
#
|
|
10
|
+
# Try Qlty Cloud: https://qlty.sh
|
|
11
|
+
#
|
|
12
|
+
# For a guide to configuration, visit https://qlty.sh/d/config
|
|
13
|
+
# Or for a full reference, visit https://qlty.sh/d/qlty-toml
|
|
14
|
+
config_version = "0"
|
|
15
|
+
|
|
16
|
+
exclude_patterns = [
|
|
17
|
+
"*_min.*",
|
|
18
|
+
"*-min.*",
|
|
19
|
+
"*.min.*",
|
|
20
|
+
"**/.yarn/**",
|
|
21
|
+
"**/*.d.ts",
|
|
22
|
+
"**/assets/**",
|
|
23
|
+
"**/bower_components/**",
|
|
24
|
+
"**/build/**",
|
|
25
|
+
"**/cache/**",
|
|
26
|
+
"**/config/**",
|
|
27
|
+
"**/db/**",
|
|
28
|
+
"**/deps/**",
|
|
29
|
+
"**/dist/**",
|
|
30
|
+
"**/extern/**",
|
|
31
|
+
"**/external/**",
|
|
32
|
+
"**/generated/**",
|
|
33
|
+
"**/Godeps/**",
|
|
34
|
+
"**/gradlew/**",
|
|
35
|
+
"**/mvnw/**",
|
|
36
|
+
"**/node_modules/**",
|
|
37
|
+
"**/protos/**",
|
|
38
|
+
"**/seed/**",
|
|
39
|
+
"**/target/**",
|
|
40
|
+
"**/templates/**",
|
|
41
|
+
"**/testdata/**",
|
|
42
|
+
"**/vendor/**",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
test_patterns = [
|
|
46
|
+
"**/test/**",
|
|
47
|
+
"**/spec/**",
|
|
48
|
+
"**/*.test.*",
|
|
49
|
+
"**/*.spec.*",
|
|
50
|
+
"**/*_test.*",
|
|
51
|
+
"**/*_spec.*",
|
|
52
|
+
"**/test_*.*",
|
|
53
|
+
"**/spec_*.*",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[smells]
|
|
57
|
+
mode = "comment"
|
|
58
|
+
|
|
59
|
+
[[source]]
|
|
60
|
+
name = "default"
|
|
61
|
+
default = true
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
[[plugin]]
|
|
65
|
+
name = "actionlint"
|
|
66
|
+
|
|
67
|
+
[[plugin]]
|
|
68
|
+
name = "osv-scanner"
|
|
69
|
+
|
|
70
|
+
[[plugin]]
|
|
71
|
+
name = "ripgrep"
|
|
72
|
+
mode = "comment"
|
|
73
|
+
|
|
74
|
+
[[plugin]]
|
|
75
|
+
name = "standardrb"
|
|
76
|
+
|
|
77
|
+
[[plugin]]
|
|
78
|
+
name = "trufflehog"
|
|
79
|
+
|
|
80
|
+
[[plugin]]
|
|
81
|
+
name = "zizmor"
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
## [0.4.0] - 2026-09-15
|
|
2
|
+
|
|
3
|
+
### Breaking changes
|
|
4
|
+
|
|
5
|
+
- Endpoints now offer `authenticated?` and `authorized?` hooks for plugging in your own auth handlers. Both default to `false` to prevent endpoints from being exposed accidentally, so every existing endpoint responds with a 401 until it overrides them. Applications upgrading from older versions must override these methods, either to return `true` or with code that handles auth.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Responds with `401 Authentication required` when `authenticated?` returns `false`, and with `403 Forbidden` when `authorized?` returns `false`. The endpoint's status, headers, and content are only consulted when both return `true`.
|
|
10
|
+
- Adds a `www_authenticate_header` hook to endpoints. When it returns a challenge, such as `Bearer realm="api"`, 401 responses include it as the `WWW-Authenticate` header. It defaults to `nil`, which omits the header.
|
|
11
|
+
- Exposes the request's `Authorization` header as `request.headers[:authorization]` so endpoints can read credentials.
|
|
12
|
+
- Responds to `HEAD` requests that fail auth with an empty body and a `content-length` header, as the Rack specification requires.
|
|
13
|
+
|
|
14
|
+
## [0.3.1] - 2026-09-07
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Responds to `HEAD` requests with an empty body, as the Rack specification requires. Status and headers, including `content-length`, still describe what the matching `GET` would return.
|
|
19
|
+
- Responds to `HEAD` requests for unknown routes with a 404 and an empty body.
|
|
20
|
+
- Builds the 404 response per request instead of reusing a frozen constant, which Rack::Lint rejects.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Adds `qlty` and `flog` tasks to the default Rake task, alongside `spec` and `standard`.
|
|
25
|
+
|
|
1
26
|
## [0.3.0] - 2026-09-06
|
|
2
27
|
|
|
3
28
|
### Breaking changes
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -50,6 +50,14 @@ Here's an example for an HTTP get to the `/hello` route, which returns a hello w
|
|
|
50
50
|
class MyEndpoint < Liberty::Endpoint
|
|
51
51
|
responds_to :get, '/hello'
|
|
52
52
|
|
|
53
|
+
def authenticated?
|
|
54
|
+
true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def authorized?
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
|
|
53
61
|
def status
|
|
54
62
|
200
|
|
55
63
|
end
|
|
@@ -83,6 +91,49 @@ end
|
|
|
83
91
|
If you configure your CORS headers before you launch your application, `Endpoints` will
|
|
84
92
|
automatically respond with the right headers.
|
|
85
93
|
|
|
94
|
+
### Authentication & Authorization
|
|
95
|
+
|
|
96
|
+
Every `Endpoint` has two hooks: `#authenticated?` and `#authorized?`. Both default to `false`,
|
|
97
|
+
so an endpoint that does not implement them responds with `401 Authentication required`. Override
|
|
98
|
+
them to plug in your own auth. Liberty checks `#authenticated?` first and responds with a 401 when
|
|
99
|
+
it returns `false`. It then checks `#authorized?` and responds with `403 Forbidden` when that
|
|
100
|
+
returns `false`. Your endpoint's status, headers, and content are only consulted when both return
|
|
101
|
+
`true`. The raw `Authorization` header is available as `request.headers[:authorization]`.
|
|
102
|
+
|
|
103
|
+
A 401 response should tell the client how to authenticate. Override `#www_authenticate_header`
|
|
104
|
+
to return the challenge, such as `Bearer realm="api"`, and Liberty adds it as the
|
|
105
|
+
`WWW-Authenticate` header on 401 responses. When it returns `nil` the header is omitted.
|
|
106
|
+
|
|
107
|
+
Here's an example of how to use `#authenticated?` and `#authorized?`:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
class YourEndpoint < Liberty::Endpoint
|
|
111
|
+
responds_to :get, "/your_endpoint"
|
|
112
|
+
|
|
113
|
+
def authenticated?
|
|
114
|
+
!current_user.nil?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def www_authenticate_header
|
|
118
|
+
'Bearer realm="api"'
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def authorized?
|
|
122
|
+
current_user.admin?
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def current_user
|
|
126
|
+
@current_user ||= Sessions.find_by_token(bearer_token)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def bearer_token
|
|
132
|
+
request.headers[:authorization].to_s.delete_prefix("Bearer ")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
86
137
|
## Installation
|
|
87
138
|
|
|
88
139
|
Install the gem and add to the application's Gemfile by executing:
|
data/Rakefile
CHANGED
|
@@ -7,4 +7,25 @@ RSpec::Core::RakeTask.new(:spec)
|
|
|
7
7
|
|
|
8
8
|
require "standard/rake"
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
FLOG_LIMIT = 10.0
|
|
11
|
+
|
|
12
|
+
def qlty_bin
|
|
13
|
+
installed = File.expand_path("~/.qlty/bin/qlty")
|
|
14
|
+
ENV.fetch("QLTY_BIN") { File.exist?(installed) ? installed : "qlty" }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc "Run qlty lint checks and report code smells"
|
|
18
|
+
task :qlty do
|
|
19
|
+
sh qlty_bin, "check", "--all", "--no-progress"
|
|
20
|
+
sh qlty_bin, "smells", "--all", "--quiet"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc "Fail when the average flog score per method reaches #{FLOG_LIMIT}"
|
|
24
|
+
task :flog do
|
|
25
|
+
report = `bundle exec flog -m lib`
|
|
26
|
+
puts report
|
|
27
|
+
average = report[/^\s*([\d.]+): flog\/method average/, 1].to_f
|
|
28
|
+
abort("Quality gate failed: average flog #{average} is #{FLOG_LIMIT} or higher") if average >= FLOG_LIMIT
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
task default: %i[spec standard qlty flog]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Liberty
|
|
4
|
+
module Adapters
|
|
5
|
+
class ErrorResponse
|
|
6
|
+
attr_reader :endpoint
|
|
7
|
+
|
|
8
|
+
def initialize(endpoint)
|
|
9
|
+
@endpoint = endpoint
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_rack_response
|
|
13
|
+
[status, headers, rack_body]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def headers
|
|
19
|
+
{
|
|
20
|
+
Response::CONTENT_LENGTH => body.bytesize.to_s,
|
|
21
|
+
Response::CONTENT_TYPE => Response::MIME_TYPE_TEXT
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def rack_body
|
|
26
|
+
endpoint.request.head? ? [] : [body]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "error_response"
|
|
4
|
+
|
|
5
|
+
module Liberty
|
|
6
|
+
module Adapters
|
|
7
|
+
class ForbiddenResponse < ErrorResponse
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def status
|
|
11
|
+
403
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def body
|
|
15
|
+
"Forbidden"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -7,6 +7,7 @@ module Liberty
|
|
|
7
7
|
class Request
|
|
8
8
|
PARSED_BODY = "parsed_body"
|
|
9
9
|
ROUTER_PARAMS = "router.params"
|
|
10
|
+
AUTHORIZATION = "HTTP_AUTHORIZATION"
|
|
10
11
|
|
|
11
12
|
attr_reader :env
|
|
12
13
|
|
|
@@ -15,18 +16,27 @@ module Liberty
|
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def headers
|
|
18
|
-
@headers ||= env ? {
|
|
19
|
-
accept_media_types: accept_media_types,
|
|
20
|
-
preferred_media_type: preferred_media_type
|
|
21
|
-
} : {}
|
|
19
|
+
@headers ||= env ? env_headers : {}
|
|
22
20
|
end
|
|
23
21
|
|
|
24
22
|
def params
|
|
25
23
|
@params ||= env ? all_params : {}
|
|
26
24
|
end
|
|
27
25
|
|
|
26
|
+
def head?
|
|
27
|
+
env ? rack_request.head? : false
|
|
28
|
+
end
|
|
29
|
+
|
|
28
30
|
private
|
|
29
31
|
|
|
32
|
+
def env_headers
|
|
33
|
+
{
|
|
34
|
+
accept_media_types: accept_media_types,
|
|
35
|
+
preferred_media_type: preferred_media_type,
|
|
36
|
+
authorization: env[AUTHORIZATION]
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
30
40
|
def all_params
|
|
31
41
|
form_params.merge!(body_params).merge!(url_params)
|
|
32
42
|
end
|
|
@@ -69,7 +79,9 @@ module Liberty
|
|
|
69
79
|
end
|
|
70
80
|
|
|
71
81
|
def symbolize_keys(hash)
|
|
72
|
-
hash.each_with_object({})
|
|
82
|
+
hash.each_with_object({}) do |(key, value), obj|
|
|
83
|
+
obj[key.to_sym] = value
|
|
84
|
+
end
|
|
73
85
|
end
|
|
74
86
|
|
|
75
87
|
def rack_request
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "forbidden_response"
|
|
4
|
+
require_relative "unauthenticated_response"
|
|
5
|
+
|
|
3
6
|
module Liberty
|
|
4
7
|
module Adapters
|
|
5
8
|
class Response
|
|
@@ -17,15 +20,34 @@ module Liberty
|
|
|
17
20
|
end
|
|
18
21
|
|
|
19
22
|
def to_rack_response
|
|
20
|
-
|
|
23
|
+
return unauthenticated_response unless endpoint.authenticated?
|
|
24
|
+
return forbidden_response unless endpoint.authorized?
|
|
25
|
+
|
|
26
|
+
[status, headers, rack_body]
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
private
|
|
24
30
|
|
|
31
|
+
def unauthenticated_response
|
|
32
|
+
UnauthenticatedResponse.new(endpoint).to_rack_response
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def forbidden_response
|
|
36
|
+
ForbiddenResponse.new(endpoint).to_rack_response
|
|
37
|
+
end
|
|
38
|
+
|
|
25
39
|
def status
|
|
26
40
|
endpoint.status
|
|
27
41
|
end
|
|
28
42
|
|
|
43
|
+
def rack_body
|
|
44
|
+
head? ? [] : [content]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def head?
|
|
48
|
+
endpoint.request.head?
|
|
49
|
+
end
|
|
50
|
+
|
|
29
51
|
def headers
|
|
30
52
|
headers = {CONTENT_LENGTH => content_length}
|
|
31
53
|
headers[CONTENT_TYPE] = content_type if content_type
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "error_response"
|
|
4
|
+
|
|
5
|
+
module Liberty
|
|
6
|
+
module Adapters
|
|
7
|
+
class UnauthenticatedResponse < ErrorResponse
|
|
8
|
+
WWW_AUTHENTICATE = "www-authenticate"
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def status
|
|
13
|
+
401
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def headers
|
|
17
|
+
challenge ? super.merge(WWW_AUTHENTICATE => challenge) : super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def challenge
|
|
21
|
+
endpoint.www_authenticate_header
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def body
|
|
25
|
+
"Authentication required"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/liberty/endpoint.rb
CHANGED
data/lib/liberty/router.rb
CHANGED
|
@@ -15,7 +15,9 @@ module Liberty
|
|
|
15
15
|
PATH_INFO = "PATH_INFO"
|
|
16
16
|
ROUTER_PARAMS = "router.params"
|
|
17
17
|
DYNAMIC_PREFIX = /:/
|
|
18
|
-
|
|
18
|
+
NOT_FOUND_STATUS = 404
|
|
19
|
+
NOT_FOUND_BODY = "Not Found"
|
|
20
|
+
CONTENT_LENGTH = "content-length"
|
|
19
21
|
|
|
20
22
|
def initialize
|
|
21
23
|
@apps = {}
|
|
@@ -46,7 +48,7 @@ module Liberty
|
|
|
46
48
|
|
|
47
49
|
def call(env)
|
|
48
50
|
endpoint = find_endpoint(env[REQUEST_METHOD], env[PATH_INFO]) { |params| env[ROUTER_PARAMS] = params }
|
|
49
|
-
endpoint ? endpoint.call(env) :
|
|
51
|
+
endpoint ? endpoint.call(env) : not_found_response(env)
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
def print(stdout = $stdout)
|
|
@@ -55,6 +57,18 @@ module Liberty
|
|
|
55
57
|
|
|
56
58
|
private
|
|
57
59
|
|
|
60
|
+
def not_found_response(env)
|
|
61
|
+
[NOT_FOUND_STATUS, not_found_headers, not_found_body(env)]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def not_found_headers
|
|
65
|
+
{CONTENT_LENGTH => NOT_FOUND_BODY.bytesize.to_s}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def not_found_body(env)
|
|
69
|
+
(env[REQUEST_METHOD] == HEAD) ? [] : [NOT_FOUND_BODY]
|
|
70
|
+
end
|
|
71
|
+
|
|
58
72
|
def register_route(verb, path, to)
|
|
59
73
|
dynamic?(path) ? register_dynamic_route(verb, path, to) : register_static_route(verb, path, to)
|
|
60
74
|
end
|
data/lib/liberty/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: liberty
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alan Ridlehoover
|
|
@@ -102,6 +102,8 @@ executables: []
|
|
|
102
102
|
extensions: []
|
|
103
103
|
extra_rdoc_files: []
|
|
104
104
|
files:
|
|
105
|
+
- ".qlty/.gitignore"
|
|
106
|
+
- ".qlty/qlty.toml"
|
|
105
107
|
- ".rspec"
|
|
106
108
|
- ".ruby-version"
|
|
107
109
|
- ".standard.yml"
|
|
@@ -114,11 +116,14 @@ files:
|
|
|
114
116
|
- Rakefile
|
|
115
117
|
- assets/liberty.png
|
|
116
118
|
- lib/liberty.rb
|
|
119
|
+
- lib/liberty/adapters/error_response.rb
|
|
120
|
+
- lib/liberty/adapters/forbidden_response.rb
|
|
117
121
|
- lib/liberty/adapters/parsers/factory.rb
|
|
118
122
|
- lib/liberty/adapters/parsers/json.rb
|
|
119
123
|
- lib/liberty/adapters/parsers/null.rb
|
|
120
124
|
- lib/liberty/adapters/request.rb
|
|
121
125
|
- lib/liberty/adapters/response.rb
|
|
126
|
+
- lib/liberty/adapters/unauthenticated_response.rb
|
|
122
127
|
- lib/liberty/application.rb
|
|
123
128
|
- lib/liberty/cors.rb
|
|
124
129
|
- lib/liberty/cors/middleware.rb
|