From e641c5ae9de7d747b37b2e2af8af2ce2a7168f6c Mon Sep 17 00:00:00 2001 From: Carlos Palhares Date: Mon, 22 Aug 2022 18:26:22 -0300 Subject: [PATCH] Move consent (#22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial extraction of Nitro::Consent * Fix railtie * Make actions an array instead of a hash * Add Permission * Add support for default view * Unexisting view means no permission * Add support for object conditions * Extract Consent::Permissions * Update Consent rspec matchers * Add documentation * Documentation update * Allow multiple permission directories This allow engines to declare their own permissions * Add consent:permissins rails generator * Bump version * Remove Nitro module/namespace * Rename gem from nitro-consent to consent * Bump version * Remove private repository restriction * Add Travis badge * Setup rubygems deployment * Bump version * Creates a generator for spec * Make subjects an array instead of a hash Adds ruby 2.5.0 to travis test * Move methods from Rspec to consent API * Bump to 0.4 * Bump activesupport version โ€“ย clear security alert * Bump version 0.4.2 * Prevent permissions from being loaded twice * Bump version 0.4.3 * Fix flaky spec on symbol permissions * Update bundler version to satisfy CI scenario * Load permissions via load instead of require * Introduce Consent::Reloader to reload permissions in rails * Move consent preloading to after_initialize to avoid permissions loaded twice in production * Rubocop compliant code * Add basic YARD docs * Configure travis to run rubocop on test * Bump consent to v0.5.0 * Only deploy on ruby 2.5.0 * Always reload on prepare Reloading only when permisions file change breaks the reference of the subject in consent. Consider the following scenario: 1. Rails is loaded, permissions to User also are loaded 2. Any file in app changes, User model is reloaded 3. Consent has a reference to the old User definition, with a different object_id * Release v0.5.1 * Bump version 0.5.2 * License as MIT See https://tech.powerhrg.com/oss-guide/docs/publishing/publish.html * License metadata * Whitespace * Release 0.6.0 * Introduce ability#consent to consent specific permission to ability * Build Consent::Permission with keys * Do not consent invalid permission * Only apply default permissions on initialization * Remove permissions mapping * Remove unused subject#view_for * Remove unused dependency on activesupport * Update supported rubies * Update documentation * Add renovate.json * Update README.md * Release 1.0.0 * Update rake * Add pry for development * Simplify find_subjects * Add subject reference to action * Find view definition within action context * Remove unused permission class * Remove nitro directory * Fix rspec module * Fix rubocop violations * Extract Consent::Rspec::ConsentAction and ConsentView matchers * Add activerecord to consent test environment * Turn SomeModel into an activerecord * Add support for testing scope conditions * Improve messages for consent action and consent view matchers * Release v1.0.1 * Adding initial portal configs * Fixed heroes typo * Moved consent to packages * Switch consent to github workflows * Add license_finder to consent * Run consent specs against multiple rails versions * Add power's dependency decisions * Ignore stuff * Bump rake, bundler and rubocop * rubocop-powerhome autofixes * Add rubocop to CI * Add consent to power-tools README * Fix Consent's portal config * Remove duplicated license files * Update docs/README.md Co-authored-by: Jill Klang Co-authored-by: Jillian Tankersley Co-authored-by: Bruno Trecenti Co-authored-by: Ben Langfeld Co-authored-by: Renovate Bot Co-authored-by: Wade Winningham Co-authored-by: Dang Huynh Co-authored-by: Jill Klang --- .github/workflows/consent.yml | 13 + docs/README.md | 5 + mkdocs.yml | 1 - packages/audit_tracker/LICENSE.txt | 21 -- packages/consent/.gitignore | 18 ++ packages/consent/.rubocop.yml | 10 + packages/consent/.rubocop_todo.yml | 13 + packages/consent/Gemfile | 10 + packages/consent/Rakefile | 14 + packages/consent/bin/console | 15 ++ packages/consent/bin/setup | 8 + packages/consent/consent.gemspec | 32 +++ packages/consent/doc/dependency_decisions.yml | 3 + packages/consent/docs/README.md | 252 ++++++++++++++++++ packages/consent/lib/consent.rb | 90 +++++++ packages/consent/lib/consent/ability.rb | 49 ++++ packages/consent/lib/consent/action.rb | 24 ++ packages/consent/lib/consent/dsl.rb | 38 +++ packages/consent/lib/consent/railtie.rb | 26 ++ packages/consent/lib/consent/reloader.rb | 31 +++ packages/consent/lib/consent/rspec.rb | 47 ++++ .../lib/consent/rspec/consent_action.rb | 65 +++++ .../consent/lib/consent/rspec/consent_view.rb | 76 ++++++ packages/consent/lib/consent/subject.rb | 14 + packages/consent/lib/consent/version.rb | 5 + packages/consent/lib/consent/view.rb | 26 ++ .../consent/permissions_generator.rb | 21 ++ .../consent/templates/permissions.rb.erb | 18 ++ .../consent/templates/permissions_spec.rb.erb | 18 ++ packages/consent/mkdocs.yml | 5 + packages/consent/renovate.json | 18 ++ packages/consent/spec/consent/ability_spec.rb | 58 ++++ packages/consent/spec/consent/action_spec.rb | 18 ++ packages/consent/spec/consent/dsl_spec.rb | 114 ++++++++ packages/consent/spec/consent/rspec_spec.rb | 42 +++ packages/consent/spec/consent/subject_spec.rb | 16 ++ packages/consent/spec/consent/view_spec.rb | 15 ++ packages/consent/spec/consent_spec.rb | 45 ++++ .../permissions/frustration_department.rb | 5 + .../spec/permissions/lol_department.rb | 5 + .../consent/spec/permissions/some_model.rb | 31 +++ packages/consent/spec/spec_helper.rb | 29 ++ packages/consent/spec/test.db | Bin 0 -> 12288 bytes portal.yml | 16 ++ renovate.json | 1 + 45 files changed, 1359 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/consent.yml delete mode 100644 packages/audit_tracker/LICENSE.txt create mode 100644 packages/consent/.gitignore create mode 100644 packages/consent/.rubocop.yml create mode 100644 packages/consent/.rubocop_todo.yml create mode 100644 packages/consent/Gemfile create mode 100644 packages/consent/Rakefile create mode 100755 packages/consent/bin/console create mode 100755 packages/consent/bin/setup create mode 100644 packages/consent/consent.gemspec create mode 100644 packages/consent/doc/dependency_decisions.yml create mode 100644 packages/consent/docs/README.md create mode 100644 packages/consent/lib/consent.rb create mode 100644 packages/consent/lib/consent/ability.rb create mode 100644 packages/consent/lib/consent/action.rb create mode 100644 packages/consent/lib/consent/dsl.rb create mode 100644 packages/consent/lib/consent/railtie.rb create mode 100644 packages/consent/lib/consent/reloader.rb create mode 100644 packages/consent/lib/consent/rspec.rb create mode 100644 packages/consent/lib/consent/rspec/consent_action.rb create mode 100644 packages/consent/lib/consent/rspec/consent_view.rb create mode 100644 packages/consent/lib/consent/subject.rb create mode 100644 packages/consent/lib/consent/version.rb create mode 100644 packages/consent/lib/consent/view.rb create mode 100644 packages/consent/lib/generators/consent/permissions_generator.rb create mode 100644 packages/consent/lib/generators/consent/templates/permissions.rb.erb create mode 100644 packages/consent/lib/generators/consent/templates/permissions_spec.rb.erb create mode 100644 packages/consent/mkdocs.yml create mode 100644 packages/consent/renovate.json create mode 100644 packages/consent/spec/consent/ability_spec.rb create mode 100644 packages/consent/spec/consent/action_spec.rb create mode 100644 packages/consent/spec/consent/dsl_spec.rb create mode 100644 packages/consent/spec/consent/rspec_spec.rb create mode 100644 packages/consent/spec/consent/subject_spec.rb create mode 100644 packages/consent/spec/consent/view_spec.rb create mode 100644 packages/consent/spec/consent_spec.rb create mode 100644 packages/consent/spec/permissions/frustration_department.rb create mode 100644 packages/consent/spec/permissions/lol_department.rb create mode 100644 packages/consent/spec/permissions/some_model.rb create mode 100644 packages/consent/spec/spec_helper.rb create mode 100644 packages/consent/spec/test.db diff --git a/.github/workflows/consent.yml b/.github/workflows/consent.yml new file mode 100644 index 00000000..a632c417 --- /dev/null +++ b/.github/workflows/consent.yml @@ -0,0 +1,13 @@ +name: consent + +on: + push: + +jobs: + ruby: + uses: ./.github/workflows/_ruby-package.yml + with: + package: ${{ github.workflow }} + ruby: '["2.7.4", "3.1.2"]' + rails: '["7.0.3.1","6.1.6.1","6.0.5.1","5.2.8.1"]' + secrets: inherit diff --git a/docs/README.md b/docs/README.md index a926ad75..33a1e3fa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,6 +18,10 @@ AuditTracker helps you centralize data tracking configuration to be used across Lumberaxe handles logging output formatting. +[consent](https://github.com/powerhome/power-tools/blob/main/packages/consent/docs/README.md) ๐Ÿ’Ž + +Consent provides permission-based authorization. + ## Installation ๐Ÿ›  These packages are all meant to install inside of an application and aren't intended to stand alone; currently, they are all published to [RubyGems](https://rubygems.org/) and you can use standard Bundler methods to install them. @@ -63,3 +67,4 @@ These packages are maintained by [Power's](https://github.com/powerhome) Heroes ## Contributing ๐Ÿ’™ Contributions are welcome! Feel free to [open a ticket](https://github.com/powerhome/power-tools/issues/new) or a [PR](https://github.com/powerhome/power-tools/pulls). + diff --git a/mkdocs.yml b/mkdocs.yml index 0032336d..56878382 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,7 +1,6 @@ site_name: Power Tools site_description: Packages for application configuration. repo_url: https://github.com/powerhome/power-tools -edit_uri: edit/main/nitro_config/docs/ nav: - 'Home': 'README.md' plugins: diff --git a/packages/audit_tracker/LICENSE.txt b/packages/audit_tracker/LICENSE.txt deleted file mode 100644 index 469f0df7..00000000 --- a/packages/audit_tracker/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 Power Home Remodeling - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/packages/consent/.gitignore b/packages/consent/.gitignore new file mode 100644 index 00000000..89b9ede9 --- /dev/null +++ b/packages/consent/.gitignore @@ -0,0 +1,18 @@ +/.bundle/ +/.yardoc +/_yardoc/ +coverage +pkg +/spec/reports/ +**/tmp/* +!**/tmp/.gitkeep +!tmp/.gitignore +vendor/bundle +*.log +*.sqlite +*.sqlite3 +Gemfile.lock + +# Ignore uploaded files in development +/storage/* +!/storage/.keep diff --git a/packages/consent/.rubocop.yml b/packages/consent/.rubocop.yml new file mode 100644 index 00000000..945569d1 --- /dev/null +++ b/packages/consent/.rubocop.yml @@ -0,0 +1,10 @@ +inherit_from: .rubocop_todo.yml + +require: + - rubocop-powerhome + +AllCops: + TargetRubyVersion: 2.7 + +Rails: + Enabled: false diff --git a/packages/consent/.rubocop_todo.yml b/packages/consent/.rubocop_todo.yml new file mode 100644 index 00000000..1d2530af --- /dev/null +++ b/packages/consent/.rubocop_todo.yml @@ -0,0 +1,13 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2022-08-22 19:25:27 UTC using RuboCop version 1.35.1. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 3 +# Configuration parameters: AllowComments, AllowEmptyLambdas. +Lint/EmptyBlock: + Exclude: + - 'spec/consent_spec.rb' diff --git a/packages/consent/Gemfile b/packages/consent/Gemfile new file mode 100644 index 00000000..1c59a627 --- /dev/null +++ b/packages/consent/Gemfile @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +# Specify your gem's dependencies in consent.gemspec +gemspec + +rails_version = ENV.fetch("RAILS_VERSION", ">= 5") + +gem "rails", rails_version diff --git a/packages/consent/Rakefile b/packages/consent/Rakefile new file mode 100644 index 00000000..b735c85a --- /dev/null +++ b/packages/consent/Rakefile @@ -0,0 +1,14 @@ +#!/usr/bin/env rake + +# frozen_string_literal: true + +require "bundler/setup" +Bundler::GemHelper.install_tasks + +require "rspec/core/rake_task" +RSpec::Core::RakeTask.new(:spec) + +require "rubocop/rake_task" +RuboCop::RakeTask.new(:rubocop) + +task default: %i[spec rubocop] diff --git a/packages/consent/bin/console b/packages/consent/bin/console new file mode 100755 index 00000000..faff4f35 --- /dev/null +++ b/packages/consent/bin/console @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'bundler/setup' +require 'consent' + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require 'irb' +IRB.start diff --git a/packages/consent/bin/setup b/packages/consent/bin/setup new file mode 100755 index 00000000..dce67d86 --- /dev/null +++ b/packages/consent/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/packages/consent/consent.gemspec b/packages/consent/consent.gemspec new file mode 100644 index 00000000..f55b79f9 --- /dev/null +++ b/packages/consent/consent.gemspec @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +lib = File.expand_path("lib", __dir__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require "consent/version" + +Gem::Specification.new do |spec| + spec.name = "consent" + spec.version = Consent::VERSION + spec.authors = ["Carlos Palhares"] + spec.email = ["chjunior@gmail.com"] + + spec.summary = "Consent permission based authorization" + spec.description = "Consent permission based authorization" + spec.homepage = "https://github.com/powerhome/power-tools" + spec.license = "MIT" + spec.required_ruby_version = ">= 2.7" + + spec.files = `git ls-files`.split.grep_v(/^(test|spec|features)/) + spec.require_paths = ["lib"] + + spec.add_development_dependency "activerecord", ">= 5" + spec.add_development_dependency "bundler", "~> 2.1" + spec.add_development_dependency "cancancan", "~> 1.15.0" + spec.add_development_dependency "license_finder", ">= 7.0" + spec.add_development_dependency "pry-byebug", "3.9.0" + spec.add_development_dependency "rake", "~> 13" + spec.add_development_dependency "rspec", "~> 3.0" + spec.add_development_dependency "rubocop-powerhome", "0.5.0" + spec.add_development_dependency "sqlite3", "~> 1.4.2" + spec.metadata["rubygems_mfa_required"] = "true" +end diff --git a/packages/consent/doc/dependency_decisions.yml b/packages/consent/doc/dependency_decisions.yml new file mode 100644 index 00000000..f734baa9 --- /dev/null +++ b/packages/consent/doc/dependency_decisions.yml @@ -0,0 +1,3 @@ +--- +- - :inherit_from + - https://raw.githubusercontent.com/powerhome/oss-guide/master/license_rules.yml diff --git a/packages/consent/docs/README.md b/packages/consent/docs/README.md new file mode 100644 index 00000000..e2d12d3a --- /dev/null +++ b/packages/consent/docs/README.md @@ -0,0 +1,252 @@ +# Consent [![Build Status](https://travis-ci.org/powerhome/consent.svg?branch=master)](https://travis-ci.org/powerhome/consent) + +## Installation + +Add this line to your application's Gemfile: + +```ruby +gem 'consent' +``` + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install consent + +## What is Consent + +Consent makes defining permissions easier by providing a clean, concise DSL for authorization +so that all abilities do not have to be in your `Ability` +class. + +Consent takes application permissions and models them so that permissions are organized and can +be defined granularly. It does so using the following models: + +* View: A collection of objects limited by a given condition. +* Action: An action performed on top of the objects limited by the view. For example, one user could only `:view` something, while another could `:manage` it. +* Subject: Holds the scope of the actions. +* Permission: The combination of a subject, an action, and a view (or full-access). + +## What Consent Is Not + +Consent isn't a tool to enforce permissions -- it supports CanCan(Can) for that goal. + +## Subject + +The subject is the central point of a group of actions and views. It will typically +be an `ActiveRecord` class, a `:symbol`, or any Plain Old Ruby Object. + +You define a subject with the following DSL: + +```ruby +Consent.define Project, 'Our Projects' do + #in this case, Project is the subject + # and `Our Projects` is the description that makes it clear to users + # what the subject is acting upon. + โ€ฆ +end +``` + +The scope is the action that's being performed on the subject. It can be anything, but will +typically be an ActiveRecord class, a `:symbol`, or a PORO. + +For instance: + +```ruby +Consent.define :features, 'Beta Features' do + # whatever you put inside this method defines the scope +end +``` + +## Views + +Views are the rules that limit access to actions. For instance, a user may see a `Project` +from his department, but not from others. You can enforce it with a `:department` view, +as in the examples below: + +### Hash Conditions + +Probably the most commonly used. When the view can be defined using a `where` scope in +an ActiveRecord context. It follows a match condition and will return all objects that meet +the criteria: + +```ruby +Consent.define Project, 'Projects' do + view :department, "User's department only" do |user| + { department_id: user.id } + end +end +``` + +Although hash conditions (matching object's attributes) are recommended, the constraints can +be anything you want. Since Consent does not enforce the rules, those rules are directly given +to CanCan. Following [CanCan rules](https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities%3A-Best-Practice) +for defining abilities is recommended. + +### Object Conditions + +If you're not matching for equal values, then you would need to use an object condition. + +If you already have an object and want to check to see whether the user has permission to view +that specific object, you would use object conditions. + +If your needs can't be satisfied by hash conditions, it is recommended that a second condition +is given for constraining object instances. For example, if you want to restrict a view for smaller +volume projects: + +```ruby +Consent.define Project, 'Projects' do + view :small_volumes, "User's department only", + -> (user) { + ['amount < ?', user.volume_limit] + end, + -> (user, project) { + project.amount < user.volume_limit + } +end +``` + +For object conditions, the latter argument will be the referred object, while the +first will be the context given to the [Permission](#permission) (also check +[CanCan integration](#cancan-integration)). + +## Action + +An action is anything you can perform on a given subject. In the example of +Features this would look like the following using Consent's DSL: + +```ruby +Consent.define :features, 'Beta Features' do + action :beta_chat, 'Beta Chat App' +end +``` + +To associate different views to the same action: + +```ruby +Consent.define Project, 'Projects' do + # returns conditions that can be used as a matcher for objects so the matcher + # can return true or false (hash version) + view :department, "User's department only" do |user| + { department_id: user.id } + end + view :future_projects, "User's department only", + # returns a condition to be applied to a collection of objects + -> (_) { + ['starts_at > ?', Date.today] + end, + # returns true/false based on a condition -- to use this, you must pass in + # an instance of an object in order to check the permission + -> (user, project) { + project.starts_at > Date.today + } + + action :read, 'Read projects', views: [:department, :future_projects] +end +``` + +If you have a set of actions with the same set of views, you can use a +`with_defaults` block to simplify the writing: + +```ruby +with_defaults views: [:department, :small_volumes] do + action :read, 'Read projects' + action :approve, 'Approve projects' +end +``` + +## Permission + +A permission is what is consented to the user. It consentment to perform +an *action* on a limited *view* of the *subject*. It marries the three concepts +to consent an access to the user. + +## CanCan Integration + +Consent provides a CanCan ability (Consent::Ability) to integrate your +permissions with frameworks like Rails. To use it with Rails check out the +example at [Ability for Other Users](https://github.com/CanCanCommunity/cancancan/wiki/Ability-for-Other-Users) +on CanCanCan's wiki. + +In the ability you define the scope of the permissions. This is typically a +user: + +```ruby +Consent::Ability.new(user) +``` + +You'd more commonly define a subclass of `Consent::Ability`, and consent access +to the user by calling `consent`: + +```ruby +class MyAbility < Consent::Ability + def initialize(user) + super user + + consent :read, Project, :department + end +end +``` + +You can also consent full access by not specifying the view: + +```ruby + consent :read, Project +``` + +If you have a somehow manageable permission, you can consent them in batch in your ability: + +```ruby +class MyAbility < Consent::Ability + def initialize(user) + super user + + user.permissions.each do |permission| + consent permission.action, permission.subject, permission.view + end + end +end +``` + +Consenting the same permission multiple times is handled as a Union by CanCanCan: + +```ruby +class MyAbility < Consent::Ability + def initialize(user) + super user + + consent :read, Project, :department + consent :read, Project, :future_projects + end +end + +user = User.new(department_id: 13) +ability = MyAbility.new(user) + +Project.accessible_by(ability, :read).to_sql +=> SELECT * FROM projects WHERE ((department_id = 13) OR (starts_at > '2021-04-06')) +``` + +## Rails Integration + +Consent is integrated into Rails with `Consent::Railtie`. To define where +your permission files will be, use `config.consent.path`. This defaults to +`#{Rails.root}/app/permissions/` to conform to Rails' standards. + +## Development + +After checking out the repo, run `bin/setup` to install dependencies. Then, run +`rake spec` to run the tests. You can also run `bin/console` for an interactive +prompt that will allow you to experiment. + +To install this gem onto your local machine, run `bundle exec rake install`. To +release a new version, update the version number in `version.rb`, and then run +`bundle exec rake release`, which will create a git tag for the version, push +git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). + +## Contributing + +Bug reports and pull requests are welcome on GitHub at https://github.com/powerhome/consent. diff --git a/packages/consent/lib/consent.rb b/packages/consent/lib/consent.rb new file mode 100644 index 00000000..d66523f4 --- /dev/null +++ b/packages/consent/lib/consent.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require "consent/version" +require "consent/subject" +require "consent/view" +require "consent/action" +require "consent/dsl" +require "consent/ability" if defined?(CanCan) +require "consent/railtie" if defined?(Rails) + +# Consent makes defining permissions easier by providing a clean, +# concise DSL for authorization so that all abilities do not have +# to be in your `Ability` class. +module Consent + ViewNotFound = Class.new(StandardError) + + # Default views available to every permission + # + # i.e.: + # Defining a view with no conditions: + # Consent.default_views[:all] = Consent::View.new(:all, "All") + # + # @return [Hash] + def self.default_views + @default_views ||= {} + end + + # Subjects defined in Consent + # + # @return [Array] + def self.subjects + @subjects ||= [] + end + + # Finds all subjects defined with the given key + # + # @return [Array] + def self.find_subjects(subject_key) + subjects.find_all do |subject| + subject.key.eql?(subject_key) + end + end + + # Finds an action within a subject context + # + # @return [Consent::Action,nil] + def self.find_action(subject_key, action_key) + find_subjects(subject_key) + .flat_map(&:actions) + .find do |action| + action.key.eql?(action_key) + end + end + + # Finds a view within a subject context + # + # @return [Consent::View,nil] + def self.find_view(subject_key, action_key, view_key) + find_action(subject_key, action_key)&.then do |action| + action.views[view_key] || raise(Consent::ViewNotFound) + end + end + + # Loads all permission (ruby) files from the given directory + # and using the given mechanism (default: :require) + # + # @param paths [Array] paths where the ruby files are located + # @param mechanism [:require,:load] mechanism to load the files + def self.load_subjects!(paths, mechanism = :require) + permission_files = paths.map { |dir| File.join(dir, "*.rb") } + Dir[*permission_files].each(&Kernel.method(mechanism)) + end + + # Defines a subject with the given key, label and options + # + # i.e: + # Consent.define :users, "User management" do + # view :department, "Same department only" do |user| + # { department_id: user.department_id } + # end + # action :read, "Can view users" + # action :update, "Can edit existing user", views: :department + # end + def self.define(key, label, options = {}, &block) + defaults = options.fetch(:defaults, {}) + subjects << Subject.new(key, label).tap do |subject| + DSL.build(subject, defaults, &block) + end + end +end diff --git a/packages/consent/lib/consent/ability.rb b/packages/consent/lib/consent/ability.rb new file mode 100644 index 00000000..942007c2 --- /dev/null +++ b/packages/consent/lib/consent/ability.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Consent + # Defines a CanCan(Can)::Ability class based on a permissions hash + class Ability + include CanCan::Ability + + def initialize(*args, apply_defaults: true) + @context = *args + apply_defaults! if apply_defaults + end + + def consent!(subject: nil, action: nil, view: nil) + view = case view + when Consent::View + view + when Symbol + Consent.find_view(subject, action, view) + end + + can( + action, subject, + view&.conditions(*@context), &view&.object_conditions(*@context) + ) + end + + def consent(**kwargs) + consent!(**kwargs) + rescue Consent::ViewNotFound + nil + end + + private + + def apply_defaults! + Consent.subjects.each do |subject| + subject.actions.each do |action| + next unless action.default_view + + consent( + subject: subject.key, + action: action.key, + view: action.default_view + ) + end + end + end + end +end diff --git a/packages/consent/lib/consent/action.rb b/packages/consent/lib/consent/action.rb new file mode 100644 index 00000000..c6e4ab65 --- /dev/null +++ b/packages/consent/lib/consent/action.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Consent + class Action # :nodoc: + attr_reader :subject, :key, :label, :options + + def initialize(subject, key, label, options = {}) + @subject = subject + @key = key + @label = label + @options = options + end + + def views + @views ||= @subject.views.slice(*@options.fetch(:views, [])) + end + + def default_view + return unless @options.key?(:default_view) + + @default_view ||= @subject.views[@options[:default_view]] + end + end +end diff --git a/packages/consent/lib/consent/dsl.rb b/packages/consent/lib/consent/dsl.rb new file mode 100644 index 00000000..5da5a6af --- /dev/null +++ b/packages/consent/lib/consent/dsl.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Consent + class DSL # :nodoc: + attr_reader :subject + + def initialize(subject, defaults) + @subject = subject + @defaults = defaults + end + + def with_defaults(new_defaults, &block) + DSL.build(@subject, @defaults.merge(new_defaults), &block) + end + + # rubocop:disable Lint/UnusedBlockArgument, Security/Eval + def eval_view(key, label, collection_conditions) + view key, label do |user| + eval(collection_conditions) + end + end + # rubocop:enable Lint/UnusedBlockArgument, Security/Eval + + def view(key, label, instance = nil, collection = nil, &block) + collection ||= block + @subject.views[key] = View.new(key, label, instance, collection) + end + + def action(key, label, options = {}) + @subject.actions << Action.new(@subject, key, label, + @defaults.merge(options)) + end + + def self.build(subject, defaults = {}, &block) + DSL.new(subject, defaults).instance_eval(&block) + end + end +end diff --git a/packages/consent/lib/consent/railtie.rb b/packages/consent/lib/consent/railtie.rb new file mode 100644 index 00000000..92ccc465 --- /dev/null +++ b/packages/consent/lib/consent/railtie.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require "consent/reloader" + +module Consent + # Plugs consent permission load to the Rails class loading cycle + class Railtie < Rails::Railtie + config.before_configuration do |app| + default_path = app.root.join("app", "permissions") + config.consent = Consent::Reloader.new( + default_path, + ActiveSupport::Dependencies.mechanism + ) + end + + config.after_initialize do |app| + app.config.consent.execute + end + + initializer "initialize consent permissions reloading" do |app| + app.reloaders << config.consent + ActiveSupport::Dependencies.autoload_paths -= config.consent.paths + config.to_prepare { app.config.consent.execute } + end + end +end diff --git a/packages/consent/lib/consent/reloader.rb b/packages/consent/lib/consent/reloader.rb new file mode 100644 index 00000000..5c200167 --- /dev/null +++ b/packages/consent/lib/consent/reloader.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Consent + # Rails file reloader to detect permission changes and apply them to consent + class Reloader + attr_reader :paths + + delegate :updated?, :execute, :execute_if_updated, to: :updater + + def initialize(default_path, mechanism) + @paths = [default_path] + @mechanism = mechanism + end + + private + + def reload! + Consent.subjects.clear + Consent.load_subjects! paths, @mechanism + end + + def updater + @updater ||= ActiveSupport::FileUpdateChecker.new([], globs) { reload! } + end + + def globs + pairs = paths.map { |path| [path.to_s, %w[rb]] } + pairs.to_h + end + end +end diff --git a/packages/consent/lib/consent/rspec.rb b/packages/consent/lib/consent/rspec.rb new file mode 100644 index 00000000..0584a992 --- /dev/null +++ b/packages/consent/lib/consent/rspec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "consent" + +require_relative "rspec/consent_action" +require_relative "rspec/consent_view" + +module Consent + # RSpec helpers for consent. Given permissions are loaded, + # gives you the ability of defining permission specs like + # + # Given "users" permissions + # Consent.define :users, "User management" do + # view :department, "Same department only" do |user| + # { department_id: user.department_id } + # end + # action :read, "Can view users" + # action :update, "Can edit existing user", views: :department + # end + # + # RSpec.describe "User permissions" do + # include Consent::Rspec + # let(:user) { double(department_id: 15) } + # + # it do + # is_expected.to( + # consent_view(:department) + # .with_conditions(department_id: 15) + # .to(user) + # ) + # end + # it { is_expected.to consent_action(:read) } + # it { is_expected.to consent_action(:update).with_views(:department) } + # end + # + # Find more examples at: + # https://github.com/powerhome/consent + module Rspec + def consent_view(view_key, conditions = nil) + ConsentView.new(view_key, conditions) + end + + def consent_action(action_key) + ConsentAction.new(action_key) + end + end +end diff --git a/packages/consent/lib/consent/rspec/consent_action.rb b/packages/consent/lib/consent/rspec/consent_action.rb new file mode 100644 index 00000000..e1611e58 --- /dev/null +++ b/packages/consent/lib/consent/rspec/consent_action.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "consent" +RSpec::Support.require_rspec_support "fuzzy_matcher" + +module Consent + module Rspec + # @private + class ConsentAction + def initialize(action_key) + @action_key = action_key + end + + def with_views(*views) + @views = views + self + end + + def description + message = "consents action #{@action_key}" + "#{message} with views #{@views}" if @views + end + + def matches?(subject_key) + @subject_key = subject_key + @action = Consent.find_action(@subject_key, @action_key) + if @action && @views + RSpec::Support::FuzzyMatcher.values_match?( + @action.views.keys.sort, + @views.sort + ) + else + !@action.nil? + end + end + + def failure_message + failure_message_base "to" + end + + def failure_message_when_negated + failure_message_base "to not" + end + + private + + def failure_message_base(failure) # rubocop:disable Metrics/MethodLength + message = format( + "expected %s (%s) % provide action %s", + skey: @subject_key.to_s, sclass: @subject_key.class, + action: @action_key, failure: failure + ) + + if @action && @views + format( + "%s with views %s, but actual views are %p", + message: message, views: @views, keys: @action.views.keys + ) + else + message + end + end + end + end +end diff --git a/packages/consent/lib/consent/rspec/consent_view.rb b/packages/consent/lib/consent/rspec/consent_view.rb new file mode 100644 index 00000000..a914f6e6 --- /dev/null +++ b/packages/consent/lib/consent/rspec/consent_view.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module Consent + module Rspec + # @private + class ConsentView + def initialize(view_key, conditions) + @conditions = comparable_conditions(conditions) if conditions + @view_key = view_key + end + + def to(*context) + @context = context + self + end + + def description + message = "consents view #{@view_key}" + "#{message} with conditions #{@conditions}" if @conditions + end + + def with_conditions(conditions) + @conditions = comparable_conditions(conditions) + self + end + + def matches?(subject_key) + @subject_key = subject_key + @target = Consent.find_subjects(subject_key) + .filter_map { |subject| subject.views[@view_key]&.conditions(*@context) } + .map { |c| comparable_conditions(c) } + @target.include?(@conditions) + end + + def failure_message + failure_message_base "to" + end + + def failure_message_when_negated + failure_message_base "to not" + end + + private + + def comparable_conditions(conditions) + return conditions.to_sql if conditions.respond_to?(:to_sql) + + conditions + end + + def failure_message_base(failure) # rubocop:disable Metrics/MethodLength + message = format( + 'expected %s (%s) %s provide view %s with`\ + `%p, but', + skey: @subject_key.to_s, sclass: @subject_key.class, + view: @view_key, conditions: @conditions, fail: failure + ) + + if @target.any? + format( + "%s conditions are %p", + message: message, conditions: @target + ) + else + actual_views = Consent.find_subjects(subject_key) + .map(&:views) + .map(&:keys).flatten + format( + "%s available views are %p", + message: message, views: actual_views + ) + end + end + end + end +end diff --git a/packages/consent/lib/consent/subject.rb b/packages/consent/lib/consent/subject.rb new file mode 100644 index 00000000..b85f6fe0 --- /dev/null +++ b/packages/consent/lib/consent/subject.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Consent + class Subject # :nodoc: + attr_reader :key, :label, :actions, :views + + def initialize(key, label) + @key = key + @label = label + @actions = [] + @views = Consent.default_views.clone + end + end +end diff --git a/packages/consent/lib/consent/version.rb b/packages/consent/lib/consent/version.rb new file mode 100644 index 00000000..85d239f2 --- /dev/null +++ b/packages/consent/lib/consent/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Consent + VERSION = "1.0.1" +end diff --git a/packages/consent/lib/consent/view.rb b/packages/consent/lib/consent/view.rb new file mode 100644 index 00000000..531374a2 --- /dev/null +++ b/packages/consent/lib/consent/view.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Consent + class View # :nodoc: + attr_reader :key, :label + + def initialize(key, label, instance = nil, collection = nil) + @key = key + @label = label + @instance = instance + @collection = collection + end + + def conditions(*args) + return @collection unless @collection.respond_to?(:call) + + @collection.call(*args) + end + + def object_conditions(*args) + return @instance unless @instance.respond_to?(:curry) + + @instance.curry[*args] + end + end +end diff --git a/packages/consent/lib/generators/consent/permissions_generator.rb b/packages/consent/lib/generators/consent/permissions_generator.rb new file mode 100644 index 00000000..41f9836e --- /dev/null +++ b/packages/consent/lib/generators/consent/permissions_generator.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Consent + class PermissionsGenerator < Rails::Generators::NamedBase # :nodoc: + source_root File.expand_path("templates", __dir__) + argument :description, type: :string, required: false + + def create_permissions + template( + "permissions.rb.erb", + "app/permissions/#{file_path}.rb", + assigns: { description: description } + ) + + template( + "permissions_spec.rb.erb", + "spec/permissions/#{file_path}_spec.rb" + ) + end + end +end diff --git a/packages/consent/lib/generators/consent/templates/permissions.rb.erb b/packages/consent/lib/generators/consent/templates/permissions.rb.erb new file mode 100644 index 00000000..ddf917e6 --- /dev/null +++ b/packages/consent/lib/generators/consent/templates/permissions.rb.erb @@ -0,0 +1,18 @@ +Consent.define <%= @name %>, "<%= @description %>" do + # Define your views + # i.e.: + # view :department, 'Same department only' do |user| + # { department_id: user.department_id } + # end + + # Define your actions + # i.e.: + # action :read, "Can view <%= @subject %>" + + # Define actions with different views + # i.e.: + # action :update, "Can edit existing <%= @subject %>", views: :department + + # Find more examples at: + # https://github.com/powerhome/consent +end diff --git a/packages/consent/lib/generators/consent/templates/permissions_spec.rb.erb b/packages/consent/lib/generators/consent/templates/permissions_spec.rb.erb new file mode 100644 index 00000000..b8418113 --- /dev/null +++ b/packages/consent/lib/generators/consent/templates/permissions_spec.rb.erb @@ -0,0 +1,18 @@ +require 'rails_helper' + +describe '<%= @name %> permissions', type: :permissions do + subject { <%= @name %> } + # Define the user to be spec'ed + # + # let(:user) { double(entity_ids: [1, 2]) } + + # Define consent expectations for views + # + # it { is_expected.to consent_view(:entity, {entity_id: [1, 2]}).to(user) } + + # Define consent expectations for actions + # + # it { is_expected.to consent_action(:read) } + # it { is_expected.to consent_action(:update) } + # it { is_expected.to consent_action(:create) } +end diff --git a/packages/consent/mkdocs.yml b/packages/consent/mkdocs.yml new file mode 100644 index 00000000..51c97156 --- /dev/null +++ b/packages/consent/mkdocs.yml @@ -0,0 +1,5 @@ +site_name: Consent +nav: + - "Home": "README.md" +plugins: + - techdocs-core diff --git a/packages/consent/renovate.json b/packages/consent/renovate.json new file mode 100644 index 00000000..b28e16ba --- /dev/null +++ b/packages/consent/renovate.json @@ -0,0 +1,18 @@ +{ + "extends": ["config:base", "group:allNonMajor"], + "lockFileMaintenance": { + "enabled": true + }, + "labels": ["dependencies"], + "timezone": "America/New_York", + "packageRules": [ + { + "matchUpdateTypes": ["minor", "patch", "pin", "digest"], + "automerge": true + }, + { + "matchDepTypes": ["devDependencies"], + "automerge": true + } + ] +} diff --git a/packages/consent/spec/consent/ability_spec.rb b/packages/consent/spec/consent/ability_spec.rb new file mode 100644 index 00000000..9d003618 --- /dev/null +++ b/packages/consent/spec/consent/ability_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Consent::Ability do + let(:user) { double(id: 1) } + let(:ability) { Consent::Ability.new(user) } + + it "it authorizes symbol permissions" do + ability.consent subject: :beta, action: :lol_til_death + + expect(ability).to be_able_to(:lol_til_death, :beta) + end + + it "it authorizes model permissions" do + ability.consent subject: SomeModel, action: :action1 + + expect(ability).to be_able_to(:action1, SomeModel) + expect(ability).to be_able_to(:action1, SomeModel.new) + end + + it "adds view conditions to cancan conditions" do + ability.consent subject: SomeModel, action: :action1, view: :lol + + expect(ability).to be_able_to(:action1, SomeModel) + + expect(ability).to be_able_to(:action1, SomeModel.new(name: "lol")) + expect(ability).to_not be_able_to(:action1, SomeModel.new(name: "nop")) + end + + it "no permission is consented unless explicitly consented" do + expect(ability).to_not be_able_to(:action1, SomeModel) + end + + it "has the default view consented when defined" do + past = SomeModel.new(name: nil, created_at: Date.new - 10) + future = SomeModel.new(name: nil, created_at: Date.new + 10) + expect(ability).to be_able_to(:destroy, future) + expect(ability).to_not be_able_to(:destroy, past) + end + + it "cannot perform action when instance condition forbids" do + past = SomeModel.new(created_at: Date.new - 10) + + expect(ability).to_not be_able_to(:destroy, past) + end + + it "contextualizes the view/action in the subject definition" do + ability.consent subject: SomeModel, action: :create, view: :lol + ability.consent subject: SomeModel, action: :destroy, view: :lol + + create_rule = ability.send(:relevant_rules, :create, SomeModel).first + destroy_rule = ability.send(:relevant_rules, :destroy, SomeModel).first + + expect(create_rule.conditions).to eql(name: "ROFL") + expect(destroy_rule.conditions).to eql(name: "lol") + end +end diff --git a/packages/consent/spec/consent/action_spec.rb b/packages/consent/spec/consent/action_spec.rb new file mode 100644 index 00000000..c50eab9e --- /dev/null +++ b/packages/consent/spec/consent/action_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Consent::Action do + let(:view1) { Consent::View.new } + let(:subject) { Consent::Subject.new(nil, nil) } + let(:options) { { views: [:view1] } } + let(:action) { Consent::Action.new(subject, :key, "Label", options) } + + it "has a key" do + expect(action.key).to eql :key + end + + it "has a label" do + expect(action.label).to eql "Label" + end +end diff --git a/packages/consent/spec/consent/dsl_spec.rb b/packages/consent/spec/consent/dsl_spec.rb new file mode 100644 index 00000000..64ebf812 --- /dev/null +++ b/packages/consent/spec/consent/dsl_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Consent::DSL do + let(:subject) { Consent::Subject.new(nil, nil) } + let(:defaults) { {} } + let(:dsl) { Consent::DSL.new(subject, defaults) } + + describe ".build" do + it "builds the subject through the DSL" do + context_object = nil + + Consent::DSL.build subject do + context_object = self + end + + expect(context_object).to be_a(Consent::DSL) + end + + it "builds defines the defaults" do + context_defaults = nil + + Consent::DSL.build subject, default: :whatever do + context_defaults = @defaults + end + + expect(context_defaults).to eql(default: :whatever) + end + end + + describe "#view" do + it "adds a view to the subject" do + dsl.view :view_key, "View YEY" + + expect(subject.views[:view_key].label).to eql "View YEY" + end + + it "accepts a block for conditions" do + dsl.view :view_key, "View YEY" do |user| + { id: user.id } + end + + user = double(id: 10) + expect(subject.views[:view_key].conditions(user)).to eql(id: user.id) + end + end + + describe "#eval_view" do + it "accepts a conditions string for eval" do + dsl.eval_view :view_key, "View YEY", "{object: 1}" + + expect(subject.views[:view_key].conditions(nil)).to eql(object: 1) + end + + it "is a view that evaluate the condition as ruby with the user variable" do + user = double(id: 1) + + dsl.eval_view :view_key, "View YEY", "{user: user.id}" + + expect(subject.views[:view_key].conditions(user)).to eql(user: 1) + end + end + + describe "#action" do + let(:view_all) { double } + let(:view_no_access) { double } + before do + subject.views[:all] = view_all + subject.views[:no_access] = view_no_access + end + + it "creates the action in the subject" do + dsl.action :action_key, "ACTIONNNNNN" + + expect(subject.actions.last.label).to eql "ACTIONNNNNN" + end + + it "creates the action with views" do + dsl.action :action_key, "ACTIONNNNNN", views: [:all] + + expect(subject.actions.last.views.keys).to eql [:all] + end + + it "creates the action in the with context defaults" do + defaults[:views] = [:all] + + dsl.action :action_key, "ACTIONNNNNN" + + expect(subject.actions.last.views.keys).to eql [:all] + end + + it "allows to override defaults" do + defaults[:views] = [:all] + + dsl.action :action_key, "ACTIONNNNNN", views: [:no_access] + + expect(subject.actions.last.views.keys).to eql [:no_access] + end + end + + describe "#with_defaults" do + it "creates a new DSL context with merged defaults" do + defaults[:foo] = "bar" + + block = ->(*) {} + expected_defaults = { lol: "rofl", foo: "bar" } + expect(Consent::DSL).to receive(:build) + .with(subject, expected_defaults, &block) + + dsl.with_defaults lol: "rofl", &block + end + end +end diff --git a/packages/consent/spec/consent/rspec_spec.rb b/packages/consent/spec/consent/rspec_spec.rb new file mode 100644 index 00000000..35633203 --- /dev/null +++ b/packages/consent/spec/consent/rspec_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "spec_helper" +require "consent/rspec" + +RSpec.describe Consent::Rspec do + include Consent::Rspec + + describe "consent_action" do + it "validates if a given subject has the given action" do + expect(SomeModel).to consent_action(:destroy) + end + + it "validates in multiple contexts of the same subject" do + expect(SomeModel).to consent_action(:create) + end + + it "validates the views in which the action is consented" do + expect(SomeModel).to consent_action(:destroy).with_views(:lol, :self) + end + end + + describe "consent_view" do + let(:user) { double(id: 13) } + + it "validates if the subject consents the resulting view given a context" do + expect(SomeModel).to consent_view(:self, owner_id: 13).to(user) + end + + it "invalidates when conditions don't match" do + expect(SomeModel).to_not consent_view(:self, owner_id: 14).to(user) + end + + it "validates when conditions are a scope" do + expect(SomeModel).to_not( + consent_view(:scoped_self) + .with_conditions(SomeModel.where(owner_id: 13)) + .to(user) + ) + end + end +end diff --git a/packages/consent/spec/consent/subject_spec.rb b/packages/consent/spec/consent/subject_spec.rb new file mode 100644 index 00000000..6a7cbc4c --- /dev/null +++ b/packages/consent/spec/consent/subject_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Consent::Subject do + subject { Consent::Subject.new(nil, nil) } + + describe "#views" do + it "starts as the default_views" do + view = double + Consent.default_views[:view1] = view + + expect(subject.views[:view1]).to be view + end + end +end diff --git a/packages/consent/spec/consent/view_spec.rb b/packages/consent/spec/consent/view_spec.rb new file mode 100644 index 00000000..5c7d672e --- /dev/null +++ b/packages/consent/spec/consent/view_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Consent::View do + let(:obj) { double(id: "1235") } + + describe "#conditions" do + it "is the callable with the given args" do + view = Consent::View.new(nil, nil, nil, ->(obj) { obj.id }) + + expect(view.conditions(obj)).to eql "1235" + end + end +end diff --git a/packages/consent/spec/consent_spec.rb b/packages/consent/spec/consent_spec.rb new file mode 100644 index 00000000..1264d319 --- /dev/null +++ b/packages/consent/spec/consent_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Consent do + describe ".define" do + it "creates a new subject with the given key and label" do + Consent.define(:lol_key, "My Label") {} + + expect(Consent.subjects.last.label).to eql "My Label" + expect(Consent.subjects.last.key).to eql :lol_key + end + + it "yields a in dsl context" do + build_context = nil + Consent.define(:lol_key, "My Label") do + build_context = self + end + + expect(build_context).to be_a(Consent::DSL) + expect(build_context.subject).to be Consent.subjects.last + end + + it "yields a in dsl context with defaults" do + defaults = { views: [:my_view] } + + block = ->(*) {} + expect(Consent::DSL).to receive(:build) + .with(an_instance_of(Consent::Subject), defaults, &block) + + Consent.define :lol_key, "My Label", defaults: defaults, &block + end + + it "allows a subject to have multiple action definitions" do + Consent.define(:lol_key, "LOL at work") {} + Consent.define(:lol_key, "LOL at home") {} + + keys = Consent.subjects.map(&:key) + labels = Consent.subjects.map(&:label) + + expect(labels).to include "LOL at work", "LOL at home" + expect(keys).to include :lol_key + end + end +end diff --git a/packages/consent/spec/permissions/frustration_department.rb b/packages/consent/spec/permissions/frustration_department.rb new file mode 100644 index 00000000..991e4d51 --- /dev/null +++ b/packages/consent/spec/permissions/frustration_department.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Consent.define :beta, "Frustration Department (Beta)" do + action :request_frustration, "Request Frustration" +end diff --git a/packages/consent/spec/permissions/lol_department.rb b/packages/consent/spec/permissions/lol_department.rb new file mode 100644 index 00000000..67eecf1c --- /dev/null +++ b/packages/consent/spec/permissions/lol_department.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Consent.define :beta, "LOL Department (Beta)" do + action :lol_til_death, "LOL Until you die" +end diff --git a/packages/consent/spec/permissions/some_model.rb b/packages/consent/spec/permissions/some_model.rb new file mode 100644 index 00000000..ce364aa1 --- /dev/null +++ b/packages/consent/spec/permissions/some_model.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +Consent.define SomeModel, "My Label" do + view :future, "Future only", + ->(_, model) { model.created_at > Date.new }, + ->(_) { ["created_at > ?", Date.new] } + + view :self, "Default view" do |user| + { owner_id: user.id } + end + + view :scoped_self, "Default view", + ->(_user, _obj) { true } + ->(user) { SomeModel.where(owner_id: user.id) } + + view :view1, "View 1" + view :lol, "Lol Only" do |_| + { name: "lol" } + end + + action :action1, "Action One", views: %i[view1 lol] + action :destroy, "Destroy", views: %i[lol self], default_view: :future +end + +Consent.define SomeModel, "Another for the model" do + view :lol, "ROFL Only" do |_| + { name: "ROFL" } + end + + action :create, "Create", views: %i[lol self] +end diff --git a/packages/consent/spec/spec_helper.rb b/packages/consent/spec/spec_helper.rb new file mode 100644 index 00000000..79e52dcc --- /dev/null +++ b/packages/consent/spec/spec_helper.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) +require "cancan" +require "cancan/matchers" +require "consent" +require "date" + +require "active_record" + +ActiveRecord::Base.establish_connection( + adapter: "sqlite3", + database: File.join(__dir__, "test.db") +) + +class SomeModel < ActiveRecord::Base + # id INTEGER PRIMARY KEY AUTOINCREMENT + # name VARCHAR(255) + # created_at DATETIME +end + +RSpec.configure do |config| + config.around(:example) do |example| + ActiveRecord::Base.transaction(&example) + end +end + +Consent.default_views[:no_access] = Consent::View.new("", "No Access") +Consent.load_subjects! [File.join(__dir__, "permissions")] diff --git a/packages/consent/spec/test.db b/packages/consent/spec/test.db new file mode 100644 index 0000000000000000000000000000000000000000..f308270b193ac2391ec93543042cfaed08a79979 GIT binary patch literal 12288 zcmeI#F-yZh6bJCTR1^fMTh}LD5*i$o?#A|zU=wRRhu{#-^h6%M)9|_L|0*fYC!Z%g z%jutqe!~Y81Rwwb2tWV=5P$##AOHafK;Su*Xe86tJQucE6`Yo{#;)G>&`` zlSBntkq)kBigxC@OH*2(S9o@