Enforce group access on Bearer API key forward-auth at use-time
The ApiKey model only validates group access on creation (user_must_have_access runs on create). The bearer path in /api/verify never re-checked, so a user removed from an application's allowed groups kept access via an existing key until it was manually revoked. Add an app.user_allowed?(user) check to authenticate_bearer_token, matching the session path, returning 401 when the user no longer has group access. Adds a regression test that revokes membership after key creation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -138,6 +138,14 @@ module Api
|
||||
return render_bearer_error("Application is inactive")
|
||||
end
|
||||
|
||||
# Re-check group membership at use-time. The ApiKey model only validates
|
||||
# access on creation, so a user removed from the app's allowed groups
|
||||
# afterwards must not keep access via an existing key.
|
||||
unless app.user_allowed?(user)
|
||||
Rails.logger.info "ForwardAuth: API key '#{api_key.name}' denied - user #{user.email_address} lacks group access to #{app.domain_pattern}"
|
||||
return render_bearer_error("Access denied: insufficient group membership")
|
||||
end
|
||||
|
||||
api_key.touch_last_used!
|
||||
|
||||
headers = app.headers_for_user(user)
|
||||
|
||||
@@ -113,6 +113,42 @@ module Api
|
||||
assert_equal "Application is inactive", json["error"]
|
||||
end
|
||||
|
||||
test "bearer token returns 401 once user is removed from allowed groups" do
|
||||
# App restricted to a specific group; user is a member when the key is made.
|
||||
group = Group.create!(name: "webdav-users")
|
||||
restricted_app = Application.create!(
|
||||
name: "Restricted WebDAV",
|
||||
slug: "restricted-webdav",
|
||||
app_type: "forward_auth",
|
||||
domain_pattern: "restricted.example.com",
|
||||
active: true
|
||||
)
|
||||
restricted_app.allowed_groups << group
|
||||
@user.groups << group
|
||||
|
||||
key = @user.api_keys.create!(name: "Restricted Key", application: restricted_app)
|
||||
token = key.plaintext_token
|
||||
|
||||
# Sanity: access works while membership stands.
|
||||
get "/api/verify", headers: {
|
||||
"Authorization" => "Bearer #{token}",
|
||||
"X-Forwarded-Host" => "restricted.example.com"
|
||||
}
|
||||
assert_response :ok
|
||||
|
||||
# Revoke group membership; the existing key must stop working.
|
||||
@user.groups.destroy(group)
|
||||
|
||||
get "/api/verify", headers: {
|
||||
"Authorization" => "Bearer #{token}",
|
||||
"X-Forwarded-Host" => "restricted.example.com"
|
||||
}
|
||||
|
||||
assert_response :unauthorized
|
||||
json = JSON.parse(response.body)
|
||||
assert_equal "Access denied: insufficient group membership", json["error"]
|
||||
end
|
||||
|
||||
test "no bearer token falls through to cookie auth" do
|
||||
# No auth header, no session -> should redirect (cookie flow)
|
||||
get "/api/verify", headers: {
|
||||
|
||||
Reference in New Issue
Block a user