├── .coveralls.yml ├── lib ├── ya_acl │ ├── version.rb │ ├── resource.rb │ ├── role.rb │ ├── result.rb │ ├── assert.rb │ ├── builder.rb │ └── acl.rb └── ya_acl.rb ├── spec ├── spec_helper.rb └── ya_acl │ ├── role_spec.rb │ ├── acl_spec.rb │ └── builder_spec.rb ├── .gitignore ├── .travis.yml ├── Gemfile ├── Rakefile ├── ya_acl.gemspec ├── LICENSE └── README.rdoc /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: QT2fJfoNCtNjOoT6LDPI9kXaIIOLKtqsZ 2 | -------------------------------------------------------------------------------- /lib/ya_acl/version.rb: -------------------------------------------------------------------------------- 1 | module YaAcl 2 | VERSION = "0.0.7" 3 | end 4 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'ya_acl' 2 | require 'coveralls' 3 | Coveralls.wear! 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | *.gem 3 | .bundle 4 | nbproject 5 | .idea 6 | Gemfile.lock 7 | *.swo 8 | *.swp 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.8.7 3 | - ree 4 | - 1.9.2 5 | - 1.9.3 6 | - rbx 7 | - jruby 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gem 'coveralls', :require => false 3 | # Specify your gem's dependencies in ya_acl.gemspec 4 | gemspec 5 | 6 | 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | require 'rspec/core/rake_task' 4 | RSpec::Core::RakeTask.new(:spec) 5 | task :default => :spec 6 | task :test => :spec 7 | -------------------------------------------------------------------------------- /lib/ya_acl/resource.rb: -------------------------------------------------------------------------------- 1 | module YaAcl 2 | class Resource 3 | attr_reader :name 4 | 5 | def initialize(name) 6 | @name = name.to_sym 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/ya_acl/role.rb: -------------------------------------------------------------------------------- 1 | module YaAcl 2 | class Role 3 | attr_reader :name, :options 4 | def initialize(name, options = {}) 5 | @name = name.to_sym 6 | @options = options 7 | end 8 | 9 | def to_s 10 | self.name 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /lib/ya_acl/result.rb: -------------------------------------------------------------------------------- 1 | module YaAcl 2 | class Result 3 | attr_reader :status, :assert, :role 4 | 5 | def initialize(status = true, role = nil, assert = nil) 6 | @status = status 7 | @assert = assert 8 | @role = role 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /spec/ya_acl/role_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe YaAcl::Role do 4 | it 'should be instance' do 5 | options = {:name => 'Administrator'} 6 | role = YaAcl::Role.new :admin, options 7 | role.name.should == :admin 8 | role.options.should == options 9 | end 10 | end -------------------------------------------------------------------------------- /lib/ya_acl.rb: -------------------------------------------------------------------------------- 1 | module YaAcl 2 | autoload :Acl, 'ya_acl/acl' 3 | autoload :Role, 'ya_acl/role' 4 | autoload :Resource, 'ya_acl/resource' 5 | autoload :Assert, 'ya_acl/assert' 6 | autoload :Result, 'ya_acl/result' 7 | autoload :Builder, 'ya_acl/builder' 8 | 9 | class AccessDeniedError < RuntimeError ; end 10 | class AssertAccessDeniedError < AccessDeniedError ; end 11 | end 12 | -------------------------------------------------------------------------------- /lib/ya_acl/assert.rb: -------------------------------------------------------------------------------- 1 | module YaAcl 2 | class Assert 3 | attr_reader :name 4 | 5 | def initialize(name, param_names, &block) 6 | @name = name.to_sym 7 | @param_names = param_names 8 | @block = block 9 | 10 | @param_names.each do |name| 11 | self.class.send :attr_accessor, name 12 | end 13 | end 14 | 15 | def allow?(params) 16 | if @param_names != (@param_names & params.keys) 17 | raise "Params for assert '#{name}': #{@param_names.inspect}" 18 | end 19 | 20 | @param_names.each do |name| 21 | self.send "#{name}=", params[name] 22 | end 23 | 24 | instance_eval &@block 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /ya_acl.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "ya_acl/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "ya_acl" 7 | s.version = YaAcl::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Mokevnin Kirill"] 10 | s.email = ["mokevnin@gmail.com"] 11 | s.homepage = "http://github.com/kaize/ya_acl" 12 | s.summary = %q{Yet Another ACL} 13 | s.description = %q{Yet Another ACL} 14 | 15 | # s.rubyforge_project = "ya_acl" 16 | 17 | s.files = `git ls-files`.split("\n") 18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 19 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 20 | s.require_paths = ["lib"] 21 | 22 | # specify any dependencies here; for example: 23 | s.add_development_dependency "rspec" 24 | s.add_runtime_dependency "rake" 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010 Kirill Mokevnin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /spec/ya_acl/acl_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe YaAcl::Acl do 4 | 5 | before do 6 | @acl = YaAcl::Acl.new 7 | @acl.add_resource(YaAcl::Resource.new(:name)) 8 | 9 | @acl.add_role YaAcl::Role.new(:admin) 10 | @acl.add_role YaAcl::Role.new(:moderator) 11 | @acl.add_role YaAcl::Role.new(:editor) 12 | @acl.add_role YaAcl::Role.new(:member) 13 | @acl.add_role YaAcl::Role.new(:guest) 14 | 15 | assert = YaAcl::Assert.new :assert, [:object_user_id, :user_id] do 16 | object_user_id == user_id 17 | end 18 | 19 | @acl.add_assert assert 20 | end 21 | 22 | it 'should be work allow?' do 23 | @acl.allow :name, :index, :admin 24 | @acl.allow :name, :index, :member 25 | @acl.allow :name, :update, :admin 26 | 27 | @acl.allow?(:name, :index, [:admin]).should be_true 28 | @acl.allow?(:name, :index, [:nobody, :admin]).should be_true 29 | @acl.allow?(:name, :index, [:nobody, :another_nobody]).should be_false 30 | @acl.allow?(:name, 'index', ['nobody']).should be_false 31 | @acl.allow?(:name, :index, [:guest]).should be_false 32 | end 33 | 34 | it 'should be work allow? with assert' do 35 | @acl.allow :name, :index, :admin 36 | @acl.allow :name, :index, :guest, :assert 37 | @acl.allow :name, :index, :member, :assert 38 | 39 | 40 | @acl.allow?(:name, :index, [:guest], :object_user_id => 3, :user_id => 4).should be_false 41 | @acl.allow?(:name, :index, [:guest], :object_user_id => 3, :user_id => 3).should be_true 42 | end 43 | 44 | it 'should be work with roles' do 45 | assert = YaAcl::Assert.new :another_assert, [:var] do 46 | var 47 | end 48 | @acl.add_assert assert 49 | @acl.allow :name, :index, :admin 50 | @acl.allow :name, :empty, :admin 51 | @acl.allow :name, :index, :guest, :another_assert 52 | 53 | @acl.allow?(:name, :empty, [:guest, :admin]).should be_true 54 | @acl.allow?(:name, :index, [:guest, :admin], :var => false).should be_true 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/ya_acl/builder.rb: -------------------------------------------------------------------------------- 1 | module YaAcl 2 | class Builder 3 | attr_accessor :acl 4 | 5 | def self.build &block 6 | builder = new block 7 | Acl.instance = builder.acl 8 | end 9 | 10 | def initialize block 11 | self.acl = Acl.new 12 | instance_eval &block 13 | end 14 | 15 | def roles(&block) 16 | instance_eval &block 17 | end 18 | 19 | def asserts(&block) 20 | instance_eval &block 21 | end 22 | 23 | def role(name, options = {}) 24 | acl.add_role Role.new(name, options) 25 | end 26 | 27 | def assert(name, param_names, &block) 28 | acl.add_assert Assert.new(name, param_names, &block) 29 | end 30 | 31 | def resources(allow, &block) 32 | @global_allow_role = allow 33 | instance_eval &block 34 | end 35 | 36 | def resource(name, allow_roles = [], &block) 37 | raise ArgumentError, 'Options "allow_roles" must be Array' unless allow_roles.is_a? Array 38 | resource_allow_roles = allow_roles << @global_allow_role 39 | resource = Resource.new(name) 40 | acl.add_resource resource 41 | PrivilegeProxy.new(resource.name, resource_allow_roles, acl, block) 42 | end 43 | 44 | class PrivilegeProxy 45 | def initialize(name, allow_roles, acl, block) 46 | @resource_name = name 47 | @allow_roles = allow_roles 48 | @acl = acl 49 | instance_eval &block 50 | end 51 | 52 | def privilege(privilege_name, roles = [], &asserts_block) 53 | all_allow_roles = roles | @allow_roles 54 | 55 | asserts = {} 56 | if block_given? 57 | asserts = PrivilegeAssertProxy.build asserts_block, all_allow_roles 58 | end 59 | 60 | all_allow_roles.each do |role| 61 | if asserts[role] 62 | asserts[role].each do |assert| 63 | @acl.allow(@resource_name, privilege_name, role, assert) 64 | end 65 | else 66 | @acl.allow(@resource_name, privilege_name, role, nil) 67 | end 68 | end 69 | end 70 | end 71 | 72 | class PrivilegeAssertProxy 73 | attr_reader :asserts 74 | 75 | def self.build(block, all_allow_roles) 76 | builder = new block, all_allow_roles 77 | builder.asserts 78 | end 79 | 80 | def initialize(block, all_allow_roles) 81 | @all_allow_roles = all_allow_roles 82 | @asserts = {} 83 | instance_eval &block 84 | end 85 | 86 | def assert(name, roles = []) 87 | roles = @all_allow_roles unless roles.any? 88 | roles.each do |role| 89 | @asserts[role] ||= [] 90 | @asserts[role] << name 91 | end 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/ya_acl/acl.rb: -------------------------------------------------------------------------------- 1 | module YaAcl 2 | class Acl 3 | 4 | attr_reader :roles, :resources, :asserts 5 | 6 | class << self 7 | def instance 8 | @@acl 9 | end 10 | 11 | def instance=(v) 12 | @@acl = v 13 | end 14 | end 15 | 16 | def initialize() 17 | @acl = {} 18 | end 19 | 20 | def add_role(role) 21 | @roles ||= {} 22 | @roles[role.name] = role 23 | end 24 | 25 | def role(role_name) 26 | if !defined?(@roles) || !@roles[role_name.to_sym] 27 | raise ArgumentError, "#Role '#{role_name}' doesn't exists" 28 | end 29 | @roles[role_name.to_sym] 30 | end 31 | 32 | def add_resource(resource) 33 | @resources ||= {} 34 | @resources[resource.name] = resource 35 | end 36 | 37 | def resource(resource_name) 38 | if !defined?(@resources) || !@resources[resource_name.to_sym] 39 | raise ArgumentError, "#Resource '#{resource_name}' doesn't exists" 40 | end 41 | @resources[resource_name.to_sym] 42 | end 43 | 44 | def privilege(resource_name, privilege_name) 45 | r = resource(resource_name) 46 | p = privilege_name.to_sym 47 | unless @acl[r.name][p] 48 | raise ArgumentError, "Undefine privilege '#{privilege_name}' for resource '#{resource_name}'" 49 | end 50 | 51 | @acl[r.name][p] 52 | end 53 | 54 | def add_assert(assert) 55 | @asserts ||= {} 56 | @asserts[assert.name] = assert 57 | end 58 | 59 | def assert(assert_name) 60 | if !defined?(@asserts) || !@asserts[assert_name.to_sym] 61 | raise ArgumentError, "#Assert '#{assert_name}' doesn't exists" 62 | end 63 | @asserts[assert_name.to_sym] 64 | end 65 | 66 | def allow(resource_name, privilege_name, role_name, assert_name = nil) 67 | resource = resource(resource_name).name 68 | privilege = privilege_name.to_sym 69 | role = role(role_name).name 70 | 71 | @acl[resource] ||= {} 72 | @acl[resource][privilege] ||= {} 73 | @acl[resource][privilege][role] ||= {} 74 | if assert_name 75 | assert = assert(assert_name) 76 | @acl[resource][privilege][role][assert.name] = assert 77 | end 78 | end 79 | 80 | def check(resource_name, privilege_name, roles = [], params = {}) 81 | a_l = privilege(resource_name, privilege_name) 82 | roles_for_check = a_l.keys & roles.map(&:to_sym) 83 | return Result.new(false) if roles_for_check.empty? # return 84 | 85 | role_for_result = nil 86 | assert_for_result = nil 87 | roles_for_check.each do |role| 88 | role_for_result = role 89 | asserts = a_l[role] 90 | return Result.new if asserts.empty? #return 91 | result = true 92 | asserts.values.each do |assert| 93 | assert_for_result = assert 94 | result = assert.allow?(params) 95 | break unless result 96 | end 97 | if result 98 | return Result.new # return 99 | end 100 | end 101 | 102 | Result.new(false, role_for_result, assert_for_result) # return 103 | end 104 | 105 | def allow?(resource_name, privilege_name, roles = [], params = {}) 106 | check(resource_name, privilege_name, roles, params).status 107 | end 108 | 109 | def check!(resource_name, privilege_name, roles = [], params = {}) 110 | result = check(resource_name, privilege_name, roles, params) 111 | return true if result.status 112 | 113 | message = "Access denied for '#{resource_name}', privilege '#{privilege_name}'" 114 | if result.assert 115 | raise AssertAccessDeniedError, message + ", role '#{result.role}' and assert '#{result.assert.name}'" 116 | else 117 | raise AccessDeniedError, message + " and roles '#{roles.inspect}'" 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | == ya_acl 2 | 3 | {Build Status}[http://travis-ci.org/kaize/ya_acl] 4 | {Coverage}[https://coveralls.io/r/kaize/ya_acl] 5 | 6 | Ya_Acl - access control list (ACL) implementation for your Ruby application. 7 | 8 | Ya_Acl provides a standalone object through which all checks are made. 9 | This means it is not tied to any framework. Note that this guide will show you only one possible way to use this component. 10 | 11 | === Installation 12 | 13 | gem install ya_acl 14 | 15 | === Keywords 16 | 17 | Resource - object to restrict access to. 18 | Privilege - action on the resource. 19 | Role - object, which can request for an access to a resource. 20 | 21 | Role(s) request for an access to the resource privileges. 22 | For example, resource "user" can have a privilege "create". 23 | 24 | === Initial conditions 25 | 26 | - By default, everything is forbidden. Further you will only be able to grant access to a particular resource, not restrict it. 27 | - All resources must be added to the acl (otherwise you will get an exception). 28 | 29 | === Key features 30 | 31 | Asserts - runtime checks, e.g. "whether logged in user is the owner of this object". 32 | Checks can be assigned to specific roles of the current privilege, not just "on the privilege". 33 | Owning multiple roles. If at least one of the user roles has access to the resource privilege, 34 | access granted. Role with global access to all resources. Passed as an argument to the `Builder::resources` 35 | method. Roles inheritance. That is, we could define a role that will automatically get all resource privileges. 36 | 37 | === Access check algorithm 38 | 39 | 1. If none of the passed roles have access to resource privilege - access denied. 40 | 2. If any, for each role we run asserts. If at least one role passed these checks - access granted. 41 | 42 | === Workflow 43 | 44 | First, initialize acl object by creating the config file 45 | (you could use the structure sample below). It should be loaded while your application starts. 46 | Although, in development environment, you may want it to be loaded before each request. 47 | 48 | YaAcl::Builder.build do 49 | roles do # Roles 50 | role :admin 51 | role :editor 52 | role :operator 53 | end 54 | 55 | asserts do # Checks 56 | assert :assert_name, [:current_user_id, :another_user_id] do 57 | current_user_id == another_user_id 58 | end 59 | 60 | assert :another_assert_name, [:current_user_id, :another_user_id] do 61 | current_user_id != another_user_id 62 | end 63 | end 64 | 65 | resources :admin do # Resources and role with admin privileges 66 | resource 'UserController', [:editor] do # Resource and roles, which have access to the all privileges of a given resource 67 | privilege :index, [:operator] # allowed for :admin, :editor, :operator 68 | privilege :edit # allowed for :admin, :editor 69 | privilege :new do 70 | assert :assert_name, [:editor] # This check will be called for role :editor 71 | assert :another_assert_name # This check will be called for :admin and :editor roles 72 | end 73 | end 74 | end 75 | end 76 | 77 | After that, acl object becomes accessible via YaAcl::Acl.instance. 78 | 79 | acl = YaAcl::Acl.instance 80 | 81 | acl.allow?('UserController', :index, [:editor, :opeartor]) # true 82 | acl.allow?('UserController', :edit, [:editor, :opeartor]) # true 83 | acl.allow?('UserController', :edit, [:opeartor]) # false 84 | acl.allow?('UserController', :new, [:admin], :current_user_id => 1, :another_user_id => 1) # true 85 | acl.allow?('UserController', :new, [:editor], :current_user_id => 1, :another_user_id => 2) # false 86 | 87 | acl#check - returns YaAcl::Result object 88 | acl#check! - returns true or throws an exception 89 | -------------------------------------------------------------------------------- /spec/ya_acl/builder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe YaAcl::Builder do 4 | it 'should be add role' do 5 | acl = YaAcl::Builder.build do 6 | roles do 7 | role :admin, :name => 'Administrator' 8 | end 9 | end 10 | 11 | acl.role(:admin).should_not be_nil 12 | end 13 | 14 | it 'should be add resource' do 15 | acl = YaAcl::Builder.build do 16 | roles do 17 | role :admin 18 | role :another_admin 19 | role :user 20 | role :operator 21 | end 22 | 23 | asserts do 24 | assert :first_assert, [:param, :param2] do 25 | param == param2 26 | end 27 | 28 | assert :another_assert, [:param, :param2] do 29 | param != param2 30 | end 31 | end 32 | 33 | resources :admin do 34 | resource :name, [:another_admin] do 35 | privilege :index, [:operator] 36 | privilege :show, [:operator] 37 | privilege :edit 38 | privilege :with_assert, [:operator] do 39 | assert :first_assert 40 | assert :another_assert, [:admin] 41 | end 42 | end 43 | end 44 | end 45 | 46 | acl.check!(:name, :index, [:admin]).should be_true 47 | acl.check!(:name, :index, [:another_admin]).should be_true 48 | acl.check!(:name, :index, [:operator]).should be_true 49 | 50 | acl.allow?(:name, :show, [:admin]).should be_true 51 | acl.allow?(:name, :show, [:user]).should be_false 52 | acl.allow?(:name, :show, [:operator]).should be_true 53 | acl.allow?(:name, :edit, [:operator]).should be_false 54 | end 55 | 56 | it 'should be raise exception for unknown role in privilegy' do 57 | lambda { 58 | YaAcl::Builder.build do 59 | roles do 60 | role :admin, :name => 'Administrator' 61 | end 62 | resources :admin do 63 | resource 'resource' do 64 | privilege :index, [:operator] 65 | end 66 | end 67 | end 68 | }.should raise_exception(ArgumentError) 69 | end 70 | 71 | it 'should be raise exception for unknown role in resource' do 72 | lambda { 73 | YaAcl::Builder.build do 74 | roles do 75 | role :admin, :name => 'Administrator' 76 | role :operator 77 | end 78 | 79 | resources :admin do 80 | resource 'resource', [:another_admin] do 81 | privilege :index, [:opeartor] 82 | end 83 | end 84 | end 85 | }.should raise_exception(ArgumentError) 86 | end 87 | 88 | it 'should be work with asserts' do 89 | acl = YaAcl::Builder.build do 90 | roles do 91 | role :admin 92 | role :another_user 93 | role :editor 94 | role :operator 95 | end 96 | 97 | asserts do 98 | assert :first, [:var] do 99 | var 100 | end 101 | 102 | assert :another, [:first] do 103 | statuses = [1, 2] 104 | statuses.include? first 105 | end 106 | 107 | assert :another2, [:first] do 108 | !!first 109 | end 110 | 111 | assert :another3, [:first] do 112 | statuses = [1, 2] 113 | statuses.include? first 114 | end 115 | 116 | assert :another4, [:first, :second] do 117 | first == second 118 | end 119 | end 120 | 121 | resources :admin do 122 | resource :name, [:editor, :operator] do 123 | privilege :create do 124 | assert :first, [:admin, :another_user] 125 | end 126 | privilege :update do 127 | assert :another, [:editor] 128 | assert :another2, [:editor, :operator] 129 | assert :another3, [:operator] 130 | assert :another4, [:operator] 131 | end 132 | end 133 | end 134 | end 135 | 136 | acl.allow?(:name, :update, [:another_user]).should be_false 137 | acl.allow?(:name, :update, [:editor], :first => true, :second => false).should be_false 138 | acl.allow?(:name, :update, [:editor], :first => false, :second => true).should be_false 139 | acl.allow?(:name, :update, [:editor], :first => 1, :second => true).should be_true 140 | acl.check!(:name, :create, [:admin], :var => 2).should be_true 141 | acl.allow?(:name, :update, [:editor], :first => 3, :second => false).should be_false 142 | acl.allow?(:name, :update, [:operator], :first => true, :second => true).should be_false 143 | acl.allow?(:name, :update, [:operator], :first => 1, :second => 1).should be_true 144 | acl.allow?(:name, :update, [:operator], :first => 3, :second => 3).should be_false 145 | end 146 | end 147 | --------------------------------------------------------------------------------