Massive refactor. Merge forward_auth into App, remove references to unimplemented OIDC federation and SAML features. Add group and user custom claims. Groups now allocate which apps a user can use
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled

This commit is contained in:
Dan Milne
2025-11-04 13:21:55 +11:00
parent 4d1bc1ab66
commit ef15db77f9
46 changed files with 341 additions and 2917 deletions

View File

@@ -1,86 +0,0 @@
require "test_helper"
class ApplicationRoleTest < ActiveSupport::TestCase
def setup
@application = applications(:kavita_app)
@role = @application.application_roles.create!(
name: "admin",
display_name: "Administrator",
description: "Full access to all features"
)
end
test "should be valid" do
assert @role.valid?
end
test "should require name" do
@role.name = ""
assert_not @role.valid?
assert_includes @role.errors[:name], "can't be blank"
end
test "should require display_name" do
@role.display_name = ""
assert_not @role.valid?
assert_includes @role.errors[:display_name], "can't be blank"
end
test "should enforce unique role name per application" do
duplicate_role = @application.application_roles.build(
name: @role.name,
display_name: "Another Admin"
)
assert_not duplicate_role.valid?
assert_includes duplicate_role.errors[:name], "has already been taken"
end
test "should allow same role name in different applications" do
other_app = Application.create!(
name: "Other App",
slug: "other-app",
app_type: "oidc"
)
other_role = other_app.application_roles.build(
name: @role.name,
display_name: "Other Admin"
)
assert other_role.valid?
end
test "should track user assignments" do
user = users(:alice)
assert_not @role.user_has_role?(user)
@role.assign_to_user!(user)
assert @role.user_has_role?(user)
assert @role.users.include?(user)
end
test "should handle role removal" do
user = users(:alice)
@role.assign_to_user!(user)
assert @role.user_has_role?(user)
@role.remove_from_user!(user)
assert_not @role.user_has_role?(user)
assert_not @role.users.include?(user)
end
test "should default to active" do
new_role = @application.application_roles.build(
name: "member",
display_name: "Member"
)
assert new_role.active?
end
test "should support default permissions" do
role_with_permissions = @application.application_roles.create!(
name: "editor",
display_name: "Editor",
permissions: { "read" => true, "write" => true, "delete" => false }
)
assert_equal({ "read" => true, "write" => true, "delete" => false }, role_with_permissions.permissions)
end
end

View File

@@ -1,395 +0,0 @@
require "test_helper"
class ForwardAuthRuleTest < ActiveSupport::TestCase
def setup
@rule = ForwardAuthRule.new(
domain_pattern: "*.example.com",
active: true
)
end
test "should be valid with valid attributes" do
assert @rule.valid?
end
test "should require domain_pattern" do
@rule.domain_pattern = ""
assert_not @rule.valid?
assert_includes @rule.errors[:domain_pattern], "can't be blank"
end
test "should require active to be boolean" do
@rule.active = nil
assert_not @rule.valid?
assert_includes @rule.errors[:active], "is not included in the list"
end
test "should normalize domain_pattern to lowercase" do
@rule.domain_pattern = "*.EXAMPLE.COM"
@rule.save!
assert_equal "*.example.com", @rule.reload.domain_pattern
end
test "should enforce unique domain_pattern" do
@rule.save!
duplicate = ForwardAuthRule.new(
domain_pattern: "*.example.com",
active: true
)
assert_not duplicate.valid?
assert_includes duplicate.errors[:domain_pattern], "has already been taken"
end
test "should match domain patterns correctly" do
@rule.save!
assert @rule.matches_domain?("app.example.com")
assert @rule.matches_domain?("api.example.com")
assert @rule.matches_domain?("sub.app.example.com")
assert_not @rule.matches_domain?("example.org")
assert_not @rule.matches_domain?("otherexample.com")
end
test "should handle exact domain matches" do
@rule.domain_pattern = "api.example.com"
@rule.save!
assert @rule.matches_domain?("api.example.com")
assert_not @rule.matches_domain?("app.example.com")
assert_not @rule.matches_domain?("sub.api.example.com")
end
test "policy_for_user should return bypass when no groups assigned" do
user = users(:one)
@rule.save!
assert_equal "bypass", @rule.policy_for_user(user)
end
test "policy_for_user should return deny for inactive rule" do
user = users(:one)
@rule.active = false
@rule.save!
assert_equal "deny", @rule.policy_for_user(user)
end
test "policy_for_user should return deny for inactive user" do
user = users(:one)
user.update!(active: false)
@rule.save!
assert_equal "deny", @rule.policy_for_user(user)
end
test "policy_for_user should return correct policy based on user groups and TOTP" do
group = groups(:one)
user_with_totp = users(:two)
user_without_totp = users(:one)
user_with_totp.totp_secret = "test_secret"
user_with_totp.save!
@rule.allowed_groups << group
user_with_totp.groups << group
user_without_totp.groups << group
@rule.save!
assert_equal "two_factor", @rule.policy_for_user(user_with_totp)
assert_equal "one_factor", @rule.policy_for_user(user_without_totp)
end
test "user_allowed? should return true when no groups assigned" do
user = users(:one)
@rule.save!
assert @rule.user_allowed?(user)
end
test "user_allowed? should return true when user in allowed groups" do
group = groups(:one)
user = users(:one)
user.groups << group
@rule.allowed_groups << group
@rule.save!
assert @rule.user_allowed?(user)
end
test "user_allowed? should return false when user not in allowed groups" do
group = groups(:one)
user = users(:one)
@rule.allowed_groups << group
@rule.save!
assert_not @rule.user_allowed?(user)
end
# Header Configuration Tests
test "effective_headers should return default headers when no custom config" do
@rule.save!
expected = ForwardAuthRule::DEFAULT_HEADERS
assert_equal expected, @rule.effective_headers
end
test "effective_headers should merge custom headers with defaults" do
@rule.save!
@rule.update!(headers_config: { user: "X-Forwarded-User", email: "X-Forwarded-Email" })
expected = ForwardAuthRule::DEFAULT_HEADERS.merge(
user: "X-Forwarded-User",
email: "X-Forwarded-Email"
)
assert_equal expected, @rule.effective_headers
end
test "headers_for_user should generate correct headers for user with groups" do
group = groups(:one)
user = users(:one)
user.groups << group
@rule.save!
headers = @rule.headers_for_user(user)
assert_equal user.email_address, headers["X-Remote-User"]
assert_equal user.email_address, headers["X-Remote-Email"]
assert_equal user.email_address, headers["X-Remote-Name"]
assert_equal group.name, headers["X-Remote-Groups"]
assert_equal "true", headers["X-Remote-Admin"]
end
test "headers_for_user should generate correct headers for user without groups" do
user = users(:one)
@rule.save!
headers = @rule.headers_for_user(user)
assert_equal user.email_address, headers["X-Remote-User"]
assert_equal user.email_address, headers["X-Remote-Email"]
assert_equal user.email_address, headers["X-Remote-Name"]
assert_nil headers["X-Remote-Groups"] # No groups, no header
assert_equal "true", headers["X-Remote-Admin"]
end
test "headers_for_user should work with custom headers" do
user = users(:one)
@rule.update!(headers_config: {
user: "X-Forwarded-User",
groups: "X-Custom-Groups"
})
headers = @rule.headers_for_user(user)
assert_equal user.email_address, headers["X-Forwarded-User"]
assert_nil headers["X-Remote-User"] # Should be overridden
assert_equal user.email_address, headers["X-Remote-Email"] # Default preserved
assert_nil headers["X-Custom-Groups"] # User has no groups
end
test "headers_for_user should return empty hash when all headers disabled" do
user = users(:one)
@rule.update!(headers_config: {
user: "",
email: "",
name: "",
groups: "",
admin: ""
})
headers = @rule.headers_for_user(user)
assert_empty headers
end
test "headers_disabled? should correctly identify disabled headers" do
@rule.save!
assert_not @rule.headers_disabled?
@rule.update!(headers_config: { user: "X-Custom-User" })
assert_not @rule.headers_disabled?
@rule.update!(headers_config: { user: "", email: "", name: "", groups: "", admin: "" })
assert @rule.headers_disabled?
end
# Additional Domain Pattern Tests
test "matches_domain? should handle complex patterns" do
@rule.save!
# Test multiple wildcards
@rule.update!(domain_pattern: "*.*.example.com")
assert @rule.matches_domain?("app.dev.example.com")
assert @rule.matches_domain?("api.staging.example.com")
assert_not @rule.matches_domain?("example.com")
assert_not @rule.matches_domain?("app.example.org")
# Test exact domain with dots
@rule.update!(domain_pattern: "api.v2.example.com")
assert @rule.matches_domain?("api.v2.example.com")
assert_not @rule.matches_domain?("api.v3.example.com")
assert_not @rule.matches_domain?("v2.api.example.com")
end
test "matches_domain? should handle case insensitivity" do
@rule.update!(domain_pattern: "*.EXAMPLE.COM")
@rule.save!
assert @rule.matches_domain?("app.example.com")
assert @rule.matches_domain?("APP.EXAMPLE.COM")
assert @rule.matches_domain?("App.Example.Com")
end
test "matches_domain? should handle empty and nil domains" do
@rule.save!
assert_not @rule.matches_domain?("")
assert_not @rule.matches_domain?(nil)
end
# Advanced Header Configuration Tests
test "headers_for_user should handle partial header configuration" do
user = users(:one)
user.groups << groups(:one)
@rule.update!(headers_config: {
user: "X-Custom-User",
email: "", # Disabled
groups: "X-Custom-Groups"
})
@rule.save!
headers = @rule.headers_for_user(user)
# Should include custom user header
assert_equal "X-Custom-User", headers.keys.find { |k| k.include?("User") }
assert_equal user.email_address, headers["X-Custom-User"]
# Should include default email header (not overridden)
assert_equal "X-Remote-Email", headers.keys.find { |k| k.include?("Email") }
assert_equal user.email_address, headers["X-Remote-Email"]
# Should include custom groups header
assert_equal "X-Custom-Groups", headers.keys.find { |k| k.include?("Groups") }
assert_equal groups(:one).name, headers["X-Custom-Groups"]
# Should include default name header (not overridden)
assert_equal "X-Remote-Name", headers.keys.find { |k| k.include?("Name") }
end
test "headers_for_user should handle user without groups when groups header configured" do
user = users(:one)
user.groups.clear # No groups
@rule.update!(headers_config: { groups: "X-Custom-Groups" })
@rule.save!
headers = @rule.headers_for_user(user)
# Should not include groups header for user with no groups
assert_nil headers["X-Custom-Groups"]
assert_nil headers["X-Remote-Groups"]
end
test "headers_for_user should handle non-admin user correctly" do
user = users(:one)
# Ensure user is not admin
user.update!(admin: false)
@rule.save!
headers = @rule.headers_for_user(user)
assert_equal "false", headers["X-Remote-Admin"]
end
test "headers_for_user should work with nil headers_config" do
user = users(:one)
@rule.update!(headers_config: nil)
@rule.save!
headers = @rule.headers_for_user(user)
# Should use default headers
assert_equal "X-Remote-User", headers.keys.find { |k| k.include?("User") }
assert_equal user.email_address, headers["X-Remote-User"]
end
test "effective_headers should handle symbol keys in headers_config" do
@rule.update!(headers_config: { user: "X-Symbol-User", email: "X-Symbol-Email" })
@rule.save!
effective = @rule.effective_headers
assert_equal "X-Symbol-User", effective[:user]
assert_equal "X-Symbol-Email", effective[:email]
assert_equal "X-Remote-Name", effective[:name] # Default
end
test "effective_headers should handle string keys in headers_config" do
@rule.update!(headers_config: { "user" => "X-String-User", "email" => "X-String-Email" })
@rule.save!
effective = @rule.effective_headers
assert_equal "X-String-User", effective[:user]
assert_equal "X-String-Email", effective[:email]
assert_equal "X-Remote-Name", effective[:name] # Default
end
# Policy and Access Control Tests
test "policy_for_user should handle user with TOTP enabled" do
user = users(:one)
user.update!(totp_secret: "test_secret")
@rule.allowed_groups << groups(:one)
user.groups << groups(:one)
@rule.save!
policy = @rule.policy_for_user(user)
assert_equal "two_factor", policy
end
test "policy_for_user should handle user without TOTP" do
user = users(:one)
user.update!(totp_secret: nil)
@rule.allowed_groups << groups(:one)
user.groups << groups(:one)
@rule.save!
policy = @rule.policy_for_user(user)
assert_equal "one_factor", policy
end
test "policy_for_user should handle user with multiple groups" do
user = users(:one)
group1 = groups(:one)
group2 = groups(:two)
@rule.allowed_groups << group1
@rule.allowed_groups << group2
user.groups << group1
@rule.save!
policy = @rule.policy_for_user(user)
assert_equal "one_factor", policy
end
test "user_allowed? should handle user with multiple groups, one allowed" do
user = users(:one)
allowed_group = groups(:one)
other_group = groups(:two)
@rule.allowed_groups << allowed_group
user.groups << allowed_group
user.groups << other_group
@rule.save!
assert @rule.user_allowed?(user)
end
test "user_allowed? should handle user with multiple groups, none allowed" do
user = users(:one)
group1 = groups(:one)
group2 = groups(:two)
# Don't add any groups to allowed_groups
user.groups << group1
user.groups << group2
@rule.save!
assert_not @rule.user_allowed?(user)
end
end

View File

@@ -1,87 +0,0 @@
require "test_helper"
class UserRoleAssignmentTest < ActiveSupport::TestCase
def setup
@application = applications(:kavita_app)
@role = @application.application_roles.create!(
name: "admin",
display_name: "Administrator"
)
@user = users(:alice)
@assignment = UserRoleAssignment.create!(
user: @user,
application_role: @role
)
end
test "should be valid" do
assert @assignment.valid?
end
test "should enforce unique user-role combination" do
duplicate_assignment = UserRoleAssignment.new(
user: @user,
application_role: @role
)
assert_not duplicate_assignment.valid?
assert_includes duplicate_assignment.errors[:user], "has already been taken"
end
test "should allow same user with different roles" do
other_role = @application.application_roles.create!(
name: "editor",
display_name: "Editor"
)
other_assignment = UserRoleAssignment.new(
user: @user,
application_role: other_role
)
assert other_assignment.valid?
end
test "should allow same role for different users" do
other_user = users(:bob)
other_assignment = UserRoleAssignment.new(
user: other_user,
application_role: @role
)
assert other_assignment.valid?
end
test "should validate source" do
@assignment.source = "invalid_source"
assert_not @assignment.valid?
assert_includes @assignment.errors[:source], "is not included in the list"
end
test "should support valid sources" do %w[oidc manual group_sync].each do |source|
@assignment.source = source
assert @assignment.valid?, "Source '#{source}' should be valid"
end
end
test "should default to oidc source" do
new_assignment = UserRoleAssignment.new(
user: @user,
application_role: @role
)
assert_equal "oidc", new_assignment.source
end
test "should support metadata" do
metadata = { "synced_at" => Time.current, "external_source" => "authentik" }
@assignment.metadata = metadata
@assignment.save
assert_equal metadata, @assignment.reload.metadata
end
test "should identify oidc managed assignments" do
@assignment.source = "oidc"
assert @assignment.sync_from_oidc?
end
test "should not identify manually managed assignments as oidc" do
@assignment.source = "manual"
assert_not @assignment.sync_from_oidc?
end
end