### Optimized Flat Scheduling Example Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/optimization.md This example shows how flattening rules improves scheduling efficiency. Conditions are evaluated independently, allowing the scheduler to skip expensive calls. ```ruby rule { a }.enable :some_ability rule { b }.enable :some_ability rule { ~c }.prevent :some_ability ``` -------------------------------- ### Install DeclarativePolicy Gem Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/README.md Add the gem to your application's Gemfile and run bundle install, or install it directly using gem install. ```ruby gem 'declarative_policy' ``` -------------------------------- ### Sub-optimal Nested Scheduling Example Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/optimization.md This example demonstrates how nested conditions can lead to sub-optimal scheduling. The scheduler re-evaluates cached conditions unnecessarily. ```ruby condition(:a, score: 1) { ... } condition(:b, score: 2) { ... } condition(:c, score: 3) { ... } rule { a & c }.enable :some_ability rule { b & c }.enable :some_ability ``` -------------------------------- ### DeclarativePolicy Example Usage Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/README.md Demonstrates defining a policy for a Vehicle class, checking user permissions for driving and selling, and printing the results. ```ruby require 'declarative_policy' class User attr_reader :name def initialize(name:) @name = name end end class Vehicle def initialize(owner:, trusted: []) @owner = owner @trusted = trusted end def owner?(user) @owner.name == user.name end def trusted?(user) @owner.name == user.name || @trusted.detect { |t| t.name == user.name } end end class VehiclePolicy < DeclarativePolicy::Base condition(:owns) { @subject.owner?(@user) } condition(:trusted) { @subject.trusted?(@user) } rule { owns }.enable :sell_vehicle rule { trusted }.enable :drive_vehicle end jack = User.new(name: 'jack') jill = User.new(name: 'jill') jacks_vehicle = Vehicle.new(owner: jack, trusted: [jill]) jills_vehicle = Vehicle.new(owner: jill, trusted: [jack]) puts "Jack can drive Jack's vehicle? -> #{DeclarativePolicy.policy_for(jack, jacks_vehicle).can?(:drive_vehicle)}" puts "Jack can drive Jill's vehicle? -> #{DeclarativePolicy.policy_for(jack, jills_vehicle).can?(:drive_vehicle)}" puts "Jack can sell Jack's vehicle? -> #{DeclarativePolicy.policy_for(jack, jacks_vehicle).can?(:sell_vehicle)}" puts "Jack can sell Jill's vehicle? -> #{DeclarativePolicy.policy_for(jack, jills_vehicle).can?(:sell_vehicle)}" ``` ```plain $ ruby example.rb Jack can drive Jack's vehicle? -> true Jack can drive Jill's vehicle? -> true Jack can sell Jack's vehicle? -> true Jack can sell Jill's vehicle? -> false ``` -------------------------------- ### Example Policy with Conditions and Rules Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/caching.md Demonstrates a realistic policy with various conditions and rules, showcasing how facts are combined to determine abilities. Conditions are re-used across rules, and non-boolean facts are cached at the policy level. ```ruby class CountryPolicy < DeclarativePolicy::Base condition(:citizen) { @user.citizen_of?(country.country_code) } condition(:eu_citizen, scope: :user) { @user.citizen_of?(*Unions::EU) } condition(:eu_member, scope: :subject) { Unions::EU.include?(country.country_code) } condition(:has_visa_waiver) { country.visa_waivers.any? { |c| @user.citizen_of?(c) } } condition(:permanent_resident) { visa_category == :permanent } condition(:has_work_visa) { visa_category == :work } condition(:has_current_visa) { has_visa_waiver? || current_visa.present? } condition(:has_business_visa) { has_visa_waiver? || has_work_visa? || visa_category == :business } condition(:full_rights, score: 20) { citizen? || permanent_resident? } condition(:banned) { country.banned_list.include?(@user) } rule { eu_member & eu_citizen }.enable :freedom_of_movement rule { full_rights | can?(:freedom_of_movement) }.enable :settle rule { can?(:settle) | has_current_visa }.enable :enter_country rule { can?(:settle) | has_business_visa }.enable :attend_meetings rule { can?(:settle) | has_work_visa }.enable :work rule { citizen }.enable :vote rule { ~citizen & ~permanent_resident }.enable :apply_for_visa rule { banned }.prevent :enter_country, :apply_for_visa def current_visa return @current_visa if defined?(@current_visa) @current_visa = country.active_visas.find_by(applicant: @user) end def visa_category current_visa&.category end def country @subject end end ``` -------------------------------- ### Condition Scope Example (User Dependent) Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/optimization.md Define conditions that depend on both user and subject for accurate caching. This condition checks if the user owns the subject. ```ruby condition(:owns_vehicle) { @user == @subject.owner } ``` -------------------------------- ### Retrieve Policy Instance with DeclarativePolicy.policy_for Source: https://context7.com/gitlab-org/ruby/llms.txt Use `DeclarativePolicy.policy_for` to get a policy object for a user and subject. An optional `cache` parameter can be provided for shared caching across multiple checks to prevent redundant computations. It also supports anonymous users and named policies. ```ruby policy = DeclarativePolicy.policy_for(current_user, @project) policy.allowed?(:read_project) # => true / false ``` ```ruby cache = {} policy = DeclarativePolicy.policy_for(current_user, @project, cache: cache) policy.allowed?(:update_project) # => true / false ``` ```ruby policy = DeclarativePolicy.policy_for(nil, @project, cache: cache) policy.allowed?(:read_project) # anonymous-specific rules apply ``` ```ruby DeclarativePolicy.configure do named_policy :global, GlobalPolicy end global_policy = DeclarativePolicy.policy_for(current_user, :global, cache: cache) global_policy.allowed?(:access_admin_panel) # => true / false ``` -------------------------------- ### Condition Scoring Example Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/optimization.md Assign scores to conditions to influence their evaluation order. Lower scores are evaluated first, and pure functions can have a score of 0. ```ruby condition(:local_db) { @subject.related_object.present? } condition(:pure, score: 0) { @subject.some_attribute? } condition(:external_api, score: API_SCORE) { ExtrnalService.get(@subject.id).ok? } ``` -------------------------------- ### Condition Scope Example (Subject Dependent) Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/optimization.md Define conditions that only depend on the subject for broader reusability. This condition checks the roadworthiness of the subject. ```ruby condition(:roadworthy) { @subject.warrant_of_fitness.current? } ``` -------------------------------- ### Evaluate Policy for User Actions Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/README.md Demonstrates how to evaluate a policy for a given user and resource. It shows how to obtain a policy instance with caching enabled and then check if the user can perform a specific action using the `can?` method. ```ruby cache = Session.current_session policy = DeclarativePolicy.policy_for(user, car, cache: cache) policy.can?(:drive_vehicle) ``` -------------------------------- ### Configure Custom Nil Policy Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/configuration.md Replace the default nil policy with a custom implementation by specifying it in a configuration block. ```ruby DeclarativePolicy.configure do nil_policy MyNilPolicy end ``` -------------------------------- ### Enable Ability with Multiple Conditions Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/optimization.md Use this syntax to enable an ability when multiple conditions are met. The order of conditions can impact performance based on their likelihood of being determinative. ```ruby rule { external_api & pure & local_db }.enable :some_ability ``` -------------------------------- ### Configure DeclarativePolicy behavior Source: https://context7.com/gitlab-org/ruby/llms.txt Customize policy lookup, nil handling, named policies, and class naming using `DeclarativePolicy.configure`. Use `configure!` to reset all configurations. ```ruby # Custom policy namespace (Policies::Foo instead of FooPolicy) DeclarativePolicy.configure do name_transformation { |name| "Policies::#{name}" } end # Custom nil policy (override default deny-all for nil subjects) class MyNilPolicy < DeclarativePolicy::Base rule { default }.enable :read_public rule { default }.prevent_all do except :read_public end end DeclarativePolicy.configure do nil_policy MyNilPolicy end # Named (intransitive) policy — for permissions with no subject class GlobalPolicy < DeclarativePolicy::Base condition(:staff_member, scope: :user) { @user.staff? } rule { staff_member }.enable :access_dashboard end DeclarativePolicy.configure do named_policy :global, GlobalPolicy end policy = DeclarativePolicy.policy_for(user, :global) policy.allowed?(:access_dashboard) # => true if user is staff # Reset all config (useful in tests) DeclarativePolicy.configure! do name_transformation { |name| "#{name}Policy" } end ``` -------------------------------- ### DeclarativePolicy.policy_for Source: https://context7.com/gitlab-org/ruby/llms.txt Retrieves or creates a policy instance for a given user and subject. Supports an optional cache object for performance optimization. ```APIDOC ## DeclarativePolicy.policy_for — Retrieve a policy instance ### Description Returns (or creates and caches) a policy object for the given user/subject pair. The optional `cache:` parameter accepts any hash-like object; passing a shared cache across multiple checks prevents redundant computation. ### Method `DeclarativePolicy.policy_for(user, subject, cache: nil)` ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body None ### Request Example ```ruby # Basic retrieval (ephemeral, per-call cache) policy = DeclarativePolicy.policy_for(current_user, @project) policy.allowed?(:read_project) # => true / false # With a shared request-scoped cache (recommended in Rails) cache = {} policy = DeclarativePolicy.policy_for(current_user, @project, cache: cache) policy.allowed?(:update_project) # => true / false # Anonymous user (nil user) policy = DeclarativePolicy.policy_for(nil, @project, cache: cache) policy.allowed?(:read_project) # anonymous-specific rules apply # Named (intransitive) ability — no subject object needed DeclarativePolicy.configure do named_policy :global, GlobalPolicy end global_policy = DeclarativePolicy.policy_for(current_user, :global, cache: cache) global_policy.allowed?(:access_admin_panel) # => true / false ``` ### Response #### Success Response (200) - **policy** (DeclarativePolicy::Base instance) - The policy object for the given user and subject. #### Response Example ```ruby # Example policy object (internal representation) # policy = # ``` ``` -------------------------------- ### Optimize caching with condition scopes Source: https://context7.com/gitlab-org/ruby/llms.txt Control cache key granularity using `scope:` option on conditions. Scopes like `:user`, `:subject`, and `:global` enable cache sharing across policy instances, improving performance for bulk operations. ```ruby class CountryPolicy < DeclarativePolicy::Base condition(:eu_citizen, scope: :user) { @user.citizen_of?(*Unions::EU) } condition(:eu_member, scope: :subject) { Unions::EU.include?(@subject.country_code) } condition(:site_maintenance, scope: :global) { MaintenanceWindow.active? } condition(:has_visa) { @subject.visas.active.exists?(user: @user) } rule { eu_citizen & eu_member }.enable :freedom_of_movement rule { site_maintenance }.prevent_all end cache = {} # eu_citizen is only evaluated ONCE for user, not for every country DeclarativePolicy.with_preferred_scope(:user) do itinerary = [france, germany, spain] itinerary.all? do |country| DeclarativePolicy.policy_for(user, country, cache: cache).allowed?(:freedom_of_movement) end end # eu_member is only evaluated ONCE for country, not for every team member DeclarativePolicy.with_preferred_scope(:subject) do team.players.all? do |player| DeclarativePolicy.policy_for(player, france, cache: cache).allowed?(:freedom_of_movement) end end ``` -------------------------------- ### Define a policy with a condition and delegate Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/defining-policies.md Define a policy for `Registration` with a `:valid` condition and use it to prevent driving if the registration is not valid. ```ruby class RegistrationPolicy < DeclarativePolicy::Base condition(:valid) { @subject.valid_for?(@user.current_location) } rule { ~valid }.prevent :drive_vehicle end ``` -------------------------------- ### Basic Policy Structure Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/defining-policies.md A basic structure for a policy class, with conventions for placing conditions, rules, and helper methods. ```ruby class VehiclePolicy < DeclarativePolicy::Base # conditions go here by convention # rules go here by convention # helper methods go last end ``` -------------------------------- ### Rule Representation in Scheduler Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/optimization.md Illustrates how the scheduler represents enabling and preventing conditions as a flat list of rules with associated outcomes. The order of evaluation is dynamic and optimized. ```pseudo [ (a, :enable), (b, :enable), (c, :enable), (x, :prevent), (y, :prevent), (z, :prevent) ] ``` -------------------------------- ### Configure Policy Name Transformation Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/configuration.md Customize how policy class names are generated by providing a block to `name_transformation`. ```ruby DeclarativePolicy.configure do name_transformation { |name| "Policies::#{name}" } end ``` -------------------------------- ### Define Default Nil Policy Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/configuration.md This is the default implementation for handling nil values, denying all permission checks. ```ruby module DeclarativePolicy class NilPolicy < DeclarativePolicy::Base rule { default }.prevent_all end end ``` -------------------------------- ### Provide Custom Cache to policy_for Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/caching.md Pass a cache object implementing specific methods to the `policy_for` method to manage caching. The cache object must support `cache[key]`, `cache.key?(key)`, and `cache[key] = value`. ```ruby DeclarativePolicy.policy_for(user, country, cache: some_cache_value) ``` -------------------------------- ### Prevent All Based on Global Condition Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/caching.md Demonstrates how to use a global condition to prevent all actions. The rule is evaluated once and applies universally. ```ruby rule { earth_destroyed_by_meteor }.prevent_all ``` -------------------------------- ### Prevent All Abilities Rule Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/defining-policies.md Use `prevent_all` to deny all abilities at once. This is useful for denying access when a precondition fails, such as a user being banned or a resource being locked. ```ruby rule { banned }.prevent_all ``` -------------------------------- ### Abstract Policy Evaluation Method Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/defining-policies.md Define a helper method to abstract policy evaluation, reusing a cache between checks to eliminate duplicated work. ```ruby def allowed?(user, ability, subject) opts = { cache: Cache.current_cache } # re-using a cache between checks eliminates duplication of work policy = DeclarativePolicy.policy_for(user, subject, opts) policy.allowed?(ability) end ``` -------------------------------- ### Define Vehicle Policy with Conditions and Rules Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/README.md Define a policy for vehicles, including conditions like ownership, access, age, and driving license status. Rules are then defined to enable or prevent actions like driving based on these conditions. The `laws` method abstracts common logic. ```ruby class VehiclePolicy < DeclarativePolicy::Base # relevant facts condition(:owns) { @subject.owner == @user } condition(:has_access_to) { @subject.owner.trusts?(@user) } condition(:old_enough_to_drive) { @user.age >= laws.minimum_age } condition(:has_driving_license) { @user.driving_license&.valid? } # expensive rules can have 'score'. Higher scores are 'more expensive' to calculate condition(:owns, score: 0) { @subject.owner == @user } condition(:has_access_to, score: 3) { @subject.owner.trusts?(@user) } condition(:intoxicated, score: 5) { @user.blood_alcohol > laws.max_blood_alcohol } # conclusions we can draw: rule { owns }.enable :drive_vehicle rule { has_access_to }.enable :drive_vehicle rule { ~old_enough_to_drive }.prevent :drive_vehicle rule { intoxicated }.prevent :drive_vehicle rule { ~has_driving_license }.prevent :drive_vehicle # we can use methods to abstract common logic def laws @subject.registration.country.driving_laws end end ``` -------------------------------- ### Blanket Deny with Exceptions in Declarative Policy Source: https://context7.com/gitlab-org/ruby/llms.txt Use `prevent_all` to block all abilities when a condition is met. Exceptions can be specified using an `except` block to allow specific abilities. ```ruby class UserPolicy < DeclarativePolicy::Base condition(:suspended) { @subject.suspended? } condition(:deactivated) { @subject.deactivated? } # Completely block all abilities for deactivated users rule { deactivated }.prevent_all # Suspended users can still read and appeal, but nothing else rule { suspended }.prevent_all do except :read_profile except :appeal_suspension # multiple abilities in one call: # except :read_profile, :appeal_suspension end rule { default }.enable :read_profile rule { default }.enable :update_profile end suspended_user_policy = DeclarativePolicy.policy_for(actor, suspended_user) suspended_user_policy.allowed?(:update_profile) # => false (prevented by prevent_all) suspended_user_policy.allowed?(:read_profile) # => true (excepted) suspended_user_policy.allowed?(:appeal_suspension) # => true (excepted) suspended_user_policy.banned? # => false (has exceptions) deactivated_policy = DeclarativePolicy.policy_for(actor, deactivated_user) deactivated_policy.banned? # => true (no exceptions; nothing can pass) ``` -------------------------------- ### Prevent all abilities except specific ones Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/defining-policies.md Use `prevent_all` with `except` to block all abilities by default, while allowing a defined set. This is useful for broad restrictions with necessary exceptions. ```ruby rule { suspended }.prevent_all do except :read except :appeal_suspension end ``` ```ruby rule { suspended }.prevent_all do except :read, :list, :appeal_suspension end ``` -------------------------------- ### Rule DSL Combinators in Declarative Policy Source: https://context7.com/gitlab-org/ruby/llms.txt Combine conditions within `rule { }` blocks using logical operators like `|` (OR), `&` (AND), `~` (NOT), `all?`, `any?`, and `none?` for complex rule definitions. ```ruby class VehiclePolicy < DeclarativePolicy::Base condition(:owns) { @subject.owner == @user } condition(:trusted) { @subject.trusted?(@user) } condition(:has_license) { @user.driving_license&.valid? } condition(:old_enough) { @user.age >= 16 } condition(:intoxicated, score: 5) { @user.blood_alcohol > 0.08 } # OR: either owns or is trusted rule { owns | trusted }.enable :drive_vehicle # AND shorthand with & rule { has_license & old_enough }.enable :drive_solo # all?() and any?() for multi-arg readability rule { all?(has_license, old_enough) }.enable :drive_highway rule { any?(owns, trusted) }.enable :park_vehicle # Negation with ~ rule { ~old_enough }.prevent :drive_vehicle rule { intoxicated }.prevent :drive_vehicle # none? rule { none?(owns, trusted) }.prevent :park_vehicle # Compound: can only sell if you own AND have license rule { owns & has_license }.enable :sell_vehicle end jack = User.new(name: 'jack', age: 20, blood_alcohol: 0.0) jills_car = Vehicle.new(owner: 'jill', trusted: [jack]) policy = DeclarativePolicy.policy_for(jack, jills_car) policy.allowed?(:drive_vehicle) # => true (trusted) policy.allowed?(:sell_vehicle) # => false (doesn't own) ``` -------------------------------- ### Use Named Policy Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/configuration.md Use a named policy by passing its symbol to `policy_for`, useful for intransitive permission checks. ```ruby policy = DeclarativePolicy.policy_for(the_user, :global) policy.allowed?(:some_ability) ``` -------------------------------- ### Policy Inheritance with Delegation in Declarative Policy Source: https://context7.com/gitlab-org/ruby/llms.txt Use `delegate` to incorporate rules from other policy classes applied to related objects. Named delegates expose their conditions directly in the parent's rule blocks. ```ruby class LicensePolicy < DeclarativePolicy::Base condition(:expired) { @subject.expires_at < Time.now } condition(:suspended) { @subject.suspended? } rule { expired | suspended }.prevent :drive_vehicle end class RegistrationPolicy < DeclarativePolicy::Base condition(:valid) { @subject.valid_for?(@user.current_location) } rule { ~valid }.prevent :drive_vehicle end class VehiclePolicy < DeclarativePolicy::Base # Anonymous delegate: rules from LicensePolicy are merged automatically delegate { @user.driving_license } # Named delegate: conditions accessible as . in rules delegate(:registration) { @subject.registration } condition(:owns) { @subject.owner == @user } rule { owns }.enable :drive_vehicle # Reference a named delegate's condition directly in a rule rule { registration.valid }.enable :enter_toll_road end # Even if owns is true, the LicensePolicy prevent rules will block drive_vehicle # if the license is expired, and RegistrationPolicy will block if not valid. policy = DeclarativePolicy.policy_for(user, vehicle, cache: {}) policy.allowed?(:drive_vehicle) # => depends on all three policies policy.allowed?(:enter_toll_road) # => depends on registration validity ``` -------------------------------- ### Flat Rule Definition Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/optimization.md Use flat rule definitions for easier optimization by the scheduler. Avoid complex logical ANDs and NOTs within a single rule. ```ruby rule { condition_a }.enable :some_ability rule { condition_b }.prevent :some_ability ``` ```ruby rule { condition_a & ~condition_b }.enable :some_ability ``` -------------------------------- ### Combine conditions with logical operators Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/defining-policies.md Combine multiple conditions using `|` (OR), `&` (AND), and `~` (NOT) within rule blocks to create complex access control logic. ```ruby # A or B rule { owns | has_access_to }.enable :drive_vehicle # A and B rule { has_driving_license & old_enough_to_drive }.enable :drive_vehicle # Not A rule { ~has_driving_license }.prevent :drive_vehicle ``` -------------------------------- ### Scheduler Evaluation Logic Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/optimization.md Provides pseudo-code for the core scheduling logic. It iteratively processes rules, prioritizing cheaper computations (like cached values), and short-circuits evaluation when a definitive outcome is reached. ```pseudo while any-enable-rule-remains?(rules) rule := pop-cheapest-remaining-rule(rules) fact := observe-io-and-update-cache rule.condition if fact and rule.prevents? return prevented else if fact and rule.enables? skip-all-other-enabling-rules! enabled? := true if enabled? return enabled else return prevented ``` -------------------------------- ### Define Named Policy Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/configuration.md Define a named policy using a configuration block, which can be referenced by a symbol. ```ruby DeclarativePolicy.configure do named_policy :global, MyGlobalPolicy end ``` -------------------------------- ### Defining Rules Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/defining-policies.md Define rules as conclusions based on conditions. Rules can enable or prevent abilities, and are combined implicitly. ```ruby rule { owns }.enable :drive_vehicle ``` ```ruby rule { has_access_to }.enable :drive_vehicle ``` ```ruby rule { ~old_enough_to_drive }.prevent :drive_vehicle ``` ```ruby rule { intoxicated | ~has_driving_license }.prevent :drive_vehicle ``` -------------------------------- ### Defining a policy class with `condition` and `rule` Source: https://context7.com/gitlab-org/ruby/llms.txt Defines a policy class inheriting from `DeclarativePolicy::Base`. Uses `condition` to declare facts and `rule` to define abilities based on these conditions. ```APIDOC ## Defining a policy class with `condition` and `rule` ### Description Policy classes inherit from `DeclarativePolicy::Base` and are automatically matched to domain objects by naming convention (`FooPolicy` ↔ `Foo`). `condition` declares a boolean fact evaluated in the context of the policy instance. `rule` wires conditions to `:enable` or `:prevent` outcomes for an ability. ### Method Define a class inheriting from `DeclarativePolicy::Base` and use `condition` and `rule` macros. ### Parameters None ### Request Example ```ruby class ProjectPolicy < DeclarativePolicy::Base # Conditions – evaluated lazily, cached automatically condition(:owner) { @subject.owner_id == @user.id } condition(:member) { @subject.members.include?(@user) } condition(:public_project, scope: :subject) { @subject.visibility == 'public' } # Expensive conditions get a higher score (evaluated later) condition(:admin, scope: :user, score: 0) { @user.admin? } condition(:banned, score: 3) { BanList.banned?(@user) } # Rules – pure logic, no I/O allowed here rule { owner | admin }.enable :update_project rule { owner }.enable :delete_project rule { owner | member | public_project }.enable :read_project rule { banned }.prevent :read_project rule { anonymous }.prevent :update_project, :delete_project # An ability can depend on another ability rule { can?(:update_project) }.enable :create_issue # Block form: one condition -> multiple abilities rule { owner }.policy do enable :transfer_project enable :archive_project end end # Usage cache = {} policy = DeclarativePolicy.policy_for(user, project, cache: cache) policy.allowed?(:read_project) # => true policy.allowed?(:delete_project) # => false policy.disallowed?(:delete_project) # => true policy.can?(:create_issue) # => true (via rule { can?(:update_project) }) ``` ### Response #### Success Response (200) - **allowed?** (Boolean) - Returns true if the ability is allowed, false otherwise. - **disallowed?** (Boolean) - Returns true if the ability is disallowed, false otherwise. - **can?** (Boolean) - Alias for `allowed?`. #### Response Example ```ruby # policy.allowed?(:read_project) => true # policy.disallowed?(:delete_project) => true ``` ``` -------------------------------- ### Add Sell Vehicle Rule to Policy Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/README.md Extend an existing policy by adding a new rule to enable the `:sell_vehicle` action, leveraging the already defined `:owns` condition. This demonstrates how policies can be extended and how conditions are reused for caching. ```ruby rule { owns }.enable :sell_vehicle ``` -------------------------------- ### Reference delegated conditions in rules Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/defining-policies.md Define a delegate for `registration` and then reference its `:valid` condition directly in a rule using dot notation. This allows policies to use conditions from their delegates. ```ruby delegate(:registration) { @subject.registration } rule { registration.valid }.enable :drive_vehicle ``` -------------------------------- ### Defining Conditions Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/defining-policies.md Define conditions as facts about the system state. Conditions have access to @user and @subject and are evaluated at most once, with memoized results. ```ruby condition(:owns) { @subject.owner == @user } ``` ```ruby condition(:has_access_to) { @subject.owner.trusts?(@user) } ``` ```ruby condition(:old_enough_to_drive) { @user.age >= laws.minimum_age } ``` ```ruby condition(:has_driving_license) { @user.driving_license&.valid? } ``` ```ruby condition(:intoxicated, score: 5) { @user.blood_alcohol > laws.max_blood_alcohol } ``` ```ruby condition(:has_access_to, score: 3) { @subject.owner.trusts?(@user) } ``` -------------------------------- ### Define a policy with a condition Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/defining-policies.md Define a policy for a specific object, like `DrivingLicense`, and include custom conditions such as `:expired` that can be used in rules. ```ruby class DrivingLicensePolicy < DeclarativePolicy::Base condition(:expired) { @subject.expires_at <= Time.current } rule { expired }.prevent :drive_vehicle end ``` -------------------------------- ### Imply abilities from other abilities Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/defining-policies.md Define rules where one ability can be enabled based on the evaluation of another ability using `can?`. This allows for hierarchical or derived permissions. ```ruby rule { can?(:drive_vehicle) }.enable :drive_taxi ``` -------------------------------- ### Rule Definition with Logical Operators Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/optimization.md Define a rule that enables an ability based on the conjunction of two facts. The scheduler can optimize this by not computing the second fact if the first one evaluates to false. ```ruby rule { fact_a & fact_b }.enable :some_ability ``` -------------------------------- ### Rule with Multiple Abilities Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/defining-policies.md Define a rule that enables multiple abilities based on a single condition. This is useful for grouping related permissions. ```ruby rule { old_enough_to_drive }.policy do enable :drive_vehicle enable :vote end ``` -------------------------------- ### Memoized Condition with Helper Method Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/defining-policies.md Define a condition that uses a helper method for memoization, useful for expensive computations or I/O operations. ```ruby condition(:full_license) { license.full? } ``` ```ruby condition(:learner_license) { license.learner? } ``` ```ruby condition(:hgv_license) { license.heavy_goods? } ``` ```ruby def license @license ||= Licenses.by_country(@user.country_of_residence).for_user(@user) end ``` -------------------------------- ### Delegate to other policies for checking abilities Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/defining-policies.md In `VehiclePolicy`, delegate checks for `driving_license` and `registration` to their respective policies. This promotes code reuse and modularity. ```ruby delegate { @user.driving_license } delegate { @subject.registration } ``` -------------------------------- ### Debug Policy Ability Evaluation Source: https://context7.com/gitlab-org/ruby/llms.txt Outputs a formatted trace of each evaluation step for an ability, showing score, description, and pass/fail/skip status. Can output to $stderr or a provided IO object. ```ruby class DocumentPolicy < DeclarativePolicy::Base condition(:author) { @subject.author_id == @user.id } condition(:reviewer) { @subject.reviewers.include?(@user) } condition(:archived) { @subject.archived? } rule { author | reviewer }.enable :read_document rule { author }.enable :edit_document rule { archived }.prevent :edit_document end policy = DeclarativePolicy.policy_for(user, document) # Outputs debug trace to $stderr by default policy.debug(:edit_document) # Example output: # + [0] author # [0] archived # Capture output for logging output = StringIO.new policy.debug(:edit_document, output) puts output.string ``` -------------------------------- ### Define Policy Class with Conditions and Rules Source: https://context7.com/gitlab-org/ruby/llms.txt Policy classes inherit from `DeclarativePolicy::Base`. `condition` declares boolean facts, evaluated lazily and cached. `rule` wires conditions to `:enable` or `:prevent` outcomes for abilities. Rules can also depend on other abilities or use a block form for multiple outcomes. ```ruby class ProjectPolicy < DeclarativePolicy::Base condition(:owner) { @subject.owner_id == @user.id } condition(:member) { @subject.members.include?(@user) } condition(:public_project, scope: :subject) { @subject.visibility == 'public' } condition(:admin, scope: :user, score: 0) { @user.admin? } condition(:banned, score: 3) { BanList.banned?(@user) } rule { owner | admin }.enable :update_project rule { owner }.enable :delete_project rule { owner | member | public_project }.enable :read_project rule { banned }.prevent :read_project rule { anonymous }.prevent :update_project, :delete_project rule { can?(:update_project) }.enable :create_issue rule { owner }.policy do enable :transfer_project enable :archive_project end end ``` ```ruby cache = {} policy = DeclarativePolicy.policy_for(user, project, cache: cache) policy.allowed?(:read_project) # => true policy.allowed?(:delete_project) # => false policy.disallowed?(:delete_project) # => true policy.can?(:create_issue) # => true (via rule { can?(:update_project) }) ``` -------------------------------- ### Check if a Policy Exists for a Subject Source: https://context7.com/gitlab-org/ruby/llms.txt Checks whether a policy class is registered for a given subject's class without raising an error. Useful for guarding calls to `policy_for`. ```ruby class Article; end class ArticlePolicy < DeclarativePolicy::Base condition(:published) { @subject.published? } rule { published }.enable :read end class Comment; end # No CommentPolicy defined article = Article.new comment = Comment.new DeclarativePolicy.policy?(article) # => true DeclarativePolicy.has_policy?(article) # => true (alias) DeclarativePolicy.policy?(comment) # => false # Guard before calling policy_for to avoid "no policy for Comment" error if DeclarativePolicy.policy?(resource) policy = DeclarativePolicy.policy_for(user, resource) policy.allowed?(:read) end ``` -------------------------------- ### Override delegated abilities Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/defining-policies.md Use `overrides` to specify abilities that should be defined directly in the current policy, rather than being inherited from delegated policies. This is useful for custom logic on specific abilities. ```ruby delegate { @subject.parent } overrides :drive_car, :watch_tv ``` -------------------------------- ### Override delegated abilities with `overrides` Source: https://context7.com/gitlab-org/ruby/llms.txt Use `overrides` to prevent specific abilities from being delegated to parent policies. This allows the child policy to define its own rules for these abilities independently. ```ruby class ParentPolicy < DeclarativePolicy::Base condition(:can_edit_all) { @user.super_editor? } rule { can_edit_all }.enable :edit rule { can_edit_all }.enable :delete end class ChildPolicy < DeclarativePolicy::Base delegate { @subject.parent } overrides :edit condition(:child_owner) { @subject.created_by == @user } rule { child_owner }.enable :edit end policy = DeclarativePolicy.policy_for(user, child_resource) policy.allowed?(:delete) # => from ParentPolicy delegate rules policy.allowed?(:edit) # => from ChildPolicy only ``` -------------------------------- ### Invalidate cache with `DeclarativePolicy.invalidate` Source: https://context7.com/gitlab-org/ruby/llms.txt Use `invalidate` to mark specific cache keys as stale, forcing recomputation of conditions and runners on the next check. This is crucial after application state changes that affect permissions. ```ruby cache = {} policy = DeclarativePolicy.policy_for(user, project, cache: cache) policy.allowed?(:update_project) # => false (e.g., user is not admin yet) user.update!(admin: true) dirty_keys = cache.keys.select { |k| k.include?("/dp/condition/#{UserPolicy.name}") } DeclarativePolicy.invalidate(cache, dirty_keys) policy.allowed?(:update_project) # => true ``` -------------------------------- ### Set Preferred Scope for Policy Checks Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/caching.md Use `DeclarativePolicy.with_preferred_scope` to inform the optimizer about the likely scope of conditions. This reduces the default score for conditions in the specified scope, making them more likely to be executed first. ```ruby cache = {} # preferring to run user-scoped conditions DeclarativePolicy.with_preferred_scope(:user) do itinerary.countries.all? do |c| DeclarativePolicy.policy_for(user, c, cache: cache).allowed?(:enter_country) end end # preferring to run subject-scoped conditions DeclarativePolicy.with_preferred_scope(:subject) do team.players.all? do |player| DeclarativePolicy.policy_for(player, c, cache: cache).allowed?(:enter_country) end end ``` -------------------------------- ### Define Global-Scoped Condition Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/caching.md Use the `:global` scope for conditions that are universally true or false, regardless of the user or subject. These conditions are computed only once per cache lifetime for all policy checks. ```ruby condition(:earth_destroyed_by_meteor, scope: global) { !Planet::Earth.exists? } ``` -------------------------------- ### Define User-Scoped Condition Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/caching.md Use the `:user` scope for conditions that only depend on the user's attributes. This ensures the condition is evaluated only once per user across multiple policy checks within the same cache lifetime. ```ruby condition(:eu_citizen, scope: :user) { @user.citizen_of?(*Unions::EU) } ``` -------------------------------- ### Define Subject-Scoped Condition Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/caching.md Use the `:subject` scope for conditions that only depend on the subject's (e.g., country) attributes. This optimizes checks when the same subject is evaluated against multiple users. ```ruby condition(:eu_member, scope: :subject) { Unions::EU.include?(country.country_code) } ``` -------------------------------- ### Invalidate Cache Entries Source: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/doc/caching.md Call `DeclarativePolicy.invalidate(cache, keys)` to remove cached condition results and mark dependent abilities as dirty. This method is the only place where the `#delete` method is called on the cache. ```ruby DeclarativePolicy.invalidate(cache, keys) ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.