do_role 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0e0656ea0457c67f9aff8a0e70bde325a857891a4e6e12df2ac9b29df757a330
4
+ data.tar.gz: b178335b7e1938fcb1aae1d49c0592995db7cc62236ec0b26bc369d08e4e4aff
5
+ SHA512:
6
+ metadata.gz: 1494eb63a948ff541d2473bb5c119b0783e14b2e10b0f36758b72e11058e647e2358c63750ce7866c2335a7bf7cf97f81f0b9e4784e7a54d1dd4ebcc079af4bf
7
+ data.tar.gz: f3b52ea6aea1c23688e5ac48ed0e4fae822e0c0ac584bd3a857ef8ac523604c26322ffe7fea0617c2bd6a8188b50b73c41a8d7541ff96bd0a7f330b09a13ba05
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # DoRole
2
+
3
+ 一个简单而灵活的Ruby on Rails角色权限管理解决方案。DoRole提供了直观的方式来管理Rails应用中的角色和权限。
4
+
5
+ ## 特性
6
+
7
+ - 简单直观的API设计
8
+ - 灵活的权限配置系统
9
+ - 支持权限继承
10
+ - 支持条件权限
11
+ - 内置权限审计功能
12
+ - 完全兼容Rails 7.0+
13
+
14
+ ## 安装
15
+
16
+ 在你的Gemfile中添加:
17
+
18
+ ```ruby
19
+ gem 'do_role'
20
+ ```
21
+
22
+ 然后执行:
23
+
24
+ ```bash
25
+ $ bundle install
26
+ ```
27
+
28
+ ## 基本配置
29
+
30
+ ### 生成迁移文件
31
+
32
+ ```bash
33
+ $ rails generate do_role:install
34
+ $ rails db:migrate
35
+ ```
36
+
37
+ ### 配置权限
38
+
39
+ 在`config/initializers/do_role.rb`中定义权限:
40
+
41
+ ```ruby
42
+ DoRole.permission_set do
43
+ # 使用namespace组织权限
44
+ namespace :posts do
45
+ permission :index
46
+ permission :create
47
+ permission :update
48
+ permission :destroy
49
+ end
50
+
51
+ namespace :users do
52
+ permission :manage
53
+ permission :view
54
+ end
55
+
56
+ # 可以添加自定义选项
57
+ namespace :admin do
58
+ permission :dashboard, description: "访问管理后台"
59
+ end
60
+
61
+ # 支持权限继承
62
+ namespace :articles do
63
+ permission :create
64
+ permission :update
65
+ permission :destroy
66
+ permission :manage, requires: [:create, :update, :destroy]
67
+ end
68
+
69
+ # 条件权限
70
+ namespace :comments do
71
+ permission :moderate, user_id: ->(user_id) { user_id == @user.id }
72
+ end
73
+ end
74
+ ```
75
+
76
+ ## 使用方法
77
+
78
+ ### 在模型中包含DoRole
79
+
80
+ ```ruby
81
+ class User < ApplicationRecord
82
+ include DoRole::Model
83
+ # 自动提供以下方法:
84
+ # - roles
85
+ # - permissions
86
+ # - has_permission?
87
+ end
88
+ ```
89
+
90
+ ### 创建和管理角色
91
+
92
+ ```ruby
93
+ # 创建角色
94
+ role = user.roles.create(name: "编辑", permissions: ["posts.create", "posts.update"])
95
+
96
+ # 添加权限
97
+ role.add_permission("posts.destroy")
98
+
99
+ # 移除权限
100
+ role.remove_permission("posts.destroy")
101
+
102
+ # 检查权限
103
+ role.has_permission?("posts.create") # => true
104
+ ```
105
+
106
+ ### 用户权限验证
107
+
108
+ ```ruby
109
+ # 检查用户是否拥有特定权限
110
+ user.has_permission?("posts.create")
111
+
112
+ # 带条件的权限检查
113
+ user.has_permission?("comments.moderate", comment)
114
+
115
+ # 获取用户所有权限
116
+ user.permissions # => ["posts.create", "posts.update", ...]
117
+ ```
118
+
119
+ ### 权限审计
120
+
121
+ ```ruby
122
+ # 启用权限审计
123
+ DoRole.configure do |config|
124
+ config.enable_permission_audit = true
125
+ end
126
+
127
+ # 查看权限变更记录
128
+ user.permission_audits
129
+ ```
130
+
131
+ ## 贡献
132
+
133
+ 1. Fork 项目
134
+ 2. 创建特性分支 (`git checkout -b my-new-feature`)
135
+ 3. 提交你的改动 (`git commit -am 'Add some feature'`)
136
+ 4. 推送到分支 (`git push origin my-new-feature`)
137
+ 5. 创建一个 Pull Request
138
+
139
+ ## 许可证
140
+
141
+ 本项目基于 [MIT License](https://opensource.org/licenses/MIT) 开源。
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ t.warning = false
11
+ end
12
+
13
+ Rake::TestTask.new(:integration) do |t|
14
+ t.libs << "test"
15
+ t.libs << "lib"
16
+ t.test_files = FileList["test/integration/**/*_test.rb"]
17
+ t.warning = false
18
+ end
19
+
20
+ task default: :test
21
+
22
+ desc "运行所有测试(单元测试和集成测试)"
23
+ task :all_tests do
24
+ Rake::Task["test"].invoke
25
+ Rake::Task["integration"].invoke
26
+ end
27
+
28
+ desc "构建和安装gem包"
29
+ task :build_and_install do
30
+ system "gem build do_role.gemspec"
31
+ system "gem install do_role-*.gem"
32
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DoRole
4
+ class Ability
5
+ attr_reader :user
6
+
7
+ def initialize(user)
8
+ @user = user
9
+ end
10
+
11
+ def can?(action, subject, *extra_args)
12
+ conditions = extra_args.extract_options!
13
+ permission_name = build_permission_name(action, subject)
14
+
15
+ return false unless has_base_permission?(permission_name)
16
+
17
+ permission = DoRole.permission_set.get(permission_name)
18
+ if permission && permission[:requires].present?
19
+ return false unless permission[:requires].all? { |req| can?(req, subject, conditions) }
20
+ end
21
+
22
+ # # 如果是对类进行检查且没有条件权限,则只检查基本权限
23
+ # return false if subject.is_a?(Class) && permission[:conditions].present?
24
+ # # 如果是对类进行检查且有条件权限,则返回false
25
+ return true if subject.is_a?(Class) #&& permission[:conditions].empty?
26
+
27
+ conditions_to_check = conditions
28
+ if permission && !permission[:conditions].empty?
29
+ conditions_to_check = permission[:conditions].merge(conditions)
30
+ end
31
+
32
+ check_conditions(subject, conditions_to_check)
33
+ end
34
+
35
+ private
36
+
37
+ def has_base_permission?(permission_name)
38
+ namespace, action = permission_name.to_s.split('.')
39
+ return false unless namespace && action
40
+ user.roles.any? { |role| role.has_permission?(permission_name) } ||
41
+ user.permissions[namespace]&.include?(action)
42
+ end
43
+
44
+ def check_conditions(subject, conditions)
45
+ return true if conditions.empty?
46
+
47
+ conditions.all? do |key, value|
48
+ subject_value = if subject.is_a?(Hash)
49
+ subject[key]
50
+ else
51
+ subject.respond_to?(key) ? subject.public_send(key) : nil
52
+ end
53
+
54
+ if value.is_a?(Proc)
55
+ instance_exec(subject_value, &value)
56
+ else
57
+ subject_value == value
58
+ end
59
+ end
60
+ end
61
+
62
+ def build_permission_name(action, subject)
63
+ subject_name = if subject.is_a?(Class)
64
+ if defined?(ActiveRecord) && subject.respond_to?(:base_class)
65
+ subject.base_class.name.underscore
66
+ else
67
+ subject.superclass.name.underscore
68
+ end
69
+ elsif subject.is_a?(Hash)
70
+ subject[:resource_type]&.to_s&.underscore || raise(ArgumentError, "resource_type is required when subject is a Hash")
71
+ else
72
+ get_base_class_name(subject.class)
73
+ end
74
+ [subject_name.pluralize, action].join('.')
75
+ end
76
+
77
+ def get_base_class_name(klass)
78
+ return klass.name.underscore if !defined?(ActiveRecord) || !klass.respond_to?(:base_class)
79
+ klass.base_class.name.underscore
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,28 @@
1
+ module DoRole
2
+ module Model
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def do_role(options = {})
7
+ role_association = options[:role_association] || :user_roles
8
+ role_class = options[:role_class] || 'DoRole::Role'
9
+
10
+ has_many role_association, dependent: :destroy
11
+ has_many :roles, through: role_association, class_name: role_class
12
+ end
13
+ end
14
+
15
+ included do
16
+ do_role
17
+ end
18
+
19
+ def has_permission?(permission)
20
+ return false unless permission
21
+ roles.any? { |role| role.has_permission?(permission) }
22
+ end
23
+
24
+ def computed_permissions
25
+ roles.map(&:computed_permissions).flatten.uniq
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DoRole
4
+ module PermissionManageable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ serialize :permissions, coder: JSON
9
+ validates :permissions, presence: true
10
+
11
+ after_initialize :initialize_permissions
12
+ end
13
+
14
+ # 检查是否拥有某个权限
15
+ def has_permission?(permission)
16
+ return false if permission.blank?
17
+ namespace, action = parse_permission(permission)
18
+ cached_permissions.dig(namespace)&.include?(action)
19
+ end
20
+
21
+ # 添加权限
22
+ def add_permission(permission)
23
+ validate_permission!(permission)
24
+ modify_permission(permission, :add)
25
+ end
26
+
27
+ # 移除权限
28
+ def remove_permission(permission)
29
+ return if permission.blank?
30
+ modify_permission(permission, :remove)
31
+ end
32
+
33
+ # 获取所有权限
34
+ def all_permissions
35
+ cached_permissions
36
+ end
37
+
38
+ # 计算完整权限路径
39
+ def computed_permissions
40
+ all_permissions.flat_map do |namespace, actions|
41
+ if actions.is_a?(Hash)
42
+ actions.flat_map do |sub_namespace, sub_actions|
43
+ Array.wrap(sub_actions).map { |action| "#{namespace}.#{sub_namespace}.#{action}" }
44
+ end
45
+ else
46
+ Array.wrap(actions).map { |action| "#{namespace}.#{action}" }
47
+ end
48
+ end
49
+ end
50
+
51
+ # 带缓存的权限
52
+ def cached_permissions
53
+ @cached_permissions ||= begin
54
+ all_permissions = permissions.deep_dup
55
+ if parent_role
56
+ parent_permissions = parent_role.cached_permissions
57
+ all_permissions.deep_merge!(parent_permissions) { |_, old_val, new_val| Array.wrap(old_val) | Array.wrap(new_val) }
58
+ end
59
+ all_permissions
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ # 初始化权限
66
+ def initialize_permissions
67
+ self.permissions = permissions.presence || {}.with_indifferent_access
68
+ self.permissions = convert_array_to_hash if permissions.is_a?(Array)
69
+ end
70
+
71
+ # 修改权限(添加或删除)
72
+ def modify_permission(permission, action)
73
+ namespace, action_name = parse_permission(permission)
74
+ current_permissions = permissions.deep_dup
75
+
76
+ *path, last = namespace.split('.')
77
+ target = path.inject(current_permissions) { |hash, ns| hash[ns] ||= {} }
78
+
79
+ case action
80
+ when :add
81
+ target[last] = Array.wrap(target[last]) | [action_name]
82
+ when :remove
83
+ if actions = target[last]
84
+ Array.wrap(actions).delete(action_name)
85
+ target.delete(last) if actions.blank?
86
+ end
87
+ end
88
+
89
+ self.permissions = current_permissions
90
+ clear_permissions_cache
91
+ save if persisted?
92
+ end
93
+
94
+ # 解析权限字符串
95
+ def parse_permission(permission)
96
+ raise ArgumentError, "Permission cannot be nil or empty" if permission.blank?
97
+ permission.to_s.strip.split('.').then { |parts| [parts[0..-2].join('.'), parts.last] }
98
+ end
99
+
100
+ # 验证权限格式
101
+ def validate_permission!(permission)
102
+ normalized = normalize_permission(permission)
103
+ raise ArgumentError, "Invalid permission format" unless normalized.match?(/\A[\w]+(?:\.[\w]+)+\z/)
104
+ raise ArgumentError, "Permission must contain only letters, numbers, and dots" unless normalized.match?(/\A[a-zA-Z0-9_.]+\z/)
105
+ raise ArgumentError if normalized != permission
106
+ end
107
+
108
+ # 规范化权限字符串
109
+ def normalize_permission(permission)
110
+ permission.to_s.strip.downcase
111
+ end
112
+
113
+ # 将数组格式的权限转换为哈希
114
+ def convert_array_to_hash
115
+ return unless permissions.is_a?(Array)
116
+ permissions.select(&:present?).map(&:strip).each_with_object({}.with_indifferent_access) do |permission, hash|
117
+ namespace, action = parse_permission(permission)
118
+ *path, last = namespace.split('.')
119
+ current = path.inject(hash) { |h, ns| h[ns] ||= {} }
120
+ current[last] = Array.wrap(current[last]) | [action]
121
+ end
122
+ end
123
+
124
+ # 清除权限缓存
125
+ def clear_permissions_cache
126
+ @cached_permissions = nil
127
+ child_roles.each(&:clear_permissions_cache)
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DoRole
4
+ module RoleModel
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ has_many :user_roles, dependent: :destroy
9
+ has_many :roles, through: :user_roles
10
+
11
+ serialize :permissions, coder: JSON
12
+
13
+ after_initialize do
14
+ self.permissions ||= {}
15
+ end
16
+ end
17
+
18
+ def has_permission?(permission)
19
+ return true if check_direct_permission(permission)
20
+ return true if roles.any? { |role| role.has_permission?(permission) }
21
+ false
22
+ end
23
+
24
+ def add_permission(permission)
25
+ self.permissions = (Array(permissions) + [permission.to_s]).uniq
26
+ end
27
+
28
+ def remove_permission(permission)
29
+ return unless permission
30
+ self.permissions = Array(permissions) - [permission.to_s]
31
+ end
32
+
33
+ def computed_permissions
34
+ role_permissions = roles.map(&:computed_permissions).flatten
35
+ direct_permissions = DoRole.permission_set.computed_permissions(permissions)
36
+ (role_permissions + direct_permissions).uniq
37
+ end
38
+
39
+ private
40
+
41
+ def check_direct_permission(permission)
42
+ return false unless permission
43
+ permissions.include?(permission.to_s)
44
+ end
45
+
46
+ def parse_permission(permission)
47
+ permission = permission.to_s.strip
48
+ namespace, action = permission.split('.')
49
+ [namespace, action]
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DoRole
4
+ class ConditionsProxy
5
+ attr_reader :conditions
6
+ def initialize(conditions)
7
+ @conditions = conditions
8
+ end
9
+
10
+ def condition(name, value)
11
+ @conditions[name.to_sym] = value
12
+ end
13
+
14
+ def requires(*permissions)
15
+ @conditions[:requires] = permissions.flatten.map(&:to_s)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DoRole
4
+ class PermissionAudit
5
+ class << self
6
+ def log_changes(action, namespace, permission_name, options = {})
7
+ @audit_logs ||= []
8
+ @audit_logs << {
9
+ action: action,
10
+ namespace: namespace,
11
+ permission_name: permission_name,
12
+ options: options,
13
+ timestamp: Time.now
14
+ }
15
+ end
16
+
17
+ def clear_logs
18
+ @audit_logs = []
19
+ end
20
+
21
+ def get_logs(filters = {})
22
+ return [] if @audit_logs.nil?
23
+
24
+ logs = @audit_logs.dup
25
+ logs = logs.select { |log| log[:namespace] == filters[:namespace] } if filters[:namespace]
26
+ logs = logs.select { |log| log[:action] == filters[:action] } if filters[:action]
27
+ logs
28
+ end
29
+
30
+ def get_recent_logs(limit = 10)
31
+ return [] if @audit_logs.nil?
32
+ @audit_logs.last(limit)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DoRole
4
+ class PermissionSet
5
+ attr_reader :registered_permissions
6
+ alias :permissions :registered_permissions
7
+
8
+ RESERVED_OPTIONS = [:priority, :description, :requires, :default, :conditions].freeze
9
+
10
+ def initialize
11
+ @registered_permissions = {}.with_indifferent_access
12
+ @namespace_stack = []
13
+ end
14
+
15
+ def draw(&block)
16
+ instance_eval(&block) if block_given?
17
+ end
18
+
19
+ def namespace(name, options = {}, &block)
20
+ raise ArgumentError, "name can't be blank" if name.blank?
21
+
22
+ name = name.to_s
23
+ @namespace_stack.push(name)
24
+
25
+ instance_eval(&block) if block_given?
26
+
27
+ @namespace_stack.pop
28
+ end
29
+
30
+ def permission(name, options = {}, &block)
31
+ name = name.to_s
32
+ raise ArgumentError, "name can't be blank" if name.blank?
33
+
34
+ conditions = if block_given?
35
+ ConditionsProxy.new({}).tap { |proxy| proxy.instance_eval(&block) }.conditions
36
+ else
37
+ non_reserved_options = options.except(*RESERVED_OPTIONS)
38
+ options.except!(*non_reserved_options.keys)
39
+ non_reserved_options
40
+ end
41
+
42
+ options.reverse_merge!(
43
+ conditions: conditions,
44
+ priority: 0,
45
+ description: "",
46
+ requires: Array.wrap(options[:requires]).map(&:to_s)
47
+ )
48
+
49
+ register_permission(name, options)
50
+ end
51
+
52
+ def get(permission_name)
53
+ permission = registered_permissions[permission_name.to_s]
54
+ return permission if permission&.key?(:priority)
55
+ nil
56
+ end
57
+
58
+ def valid_permission?(permission_name)
59
+ permission = get(permission_name)
60
+ !permission.nil?
61
+ end
62
+
63
+ def computed_permissions(base_permissions)
64
+ base_permissions
65
+ .map(&:to_s)
66
+ .select { |p| valid_permission?(p) }
67
+ .tap { |result| result.concat(collect_required_permissions(result)) }
68
+ .uniq
69
+ end
70
+
71
+ def namespaces
72
+ collect_namespaces(registered_permissions, [], []).uniq
73
+ end
74
+
75
+ def permissions_in_namespace(namespace)
76
+ current = registered_permissions.dig(*namespace.to_s.split('.'))
77
+ return {} unless current.is_a?(Hash)
78
+
79
+ flatten_permissions(current, namespace.to_s)
80
+ end
81
+
82
+ private
83
+
84
+ def collect_required_permissions(permissions)
85
+ permissions.flat_map do |permission|
86
+ permission_config = get(permission)
87
+ next [] unless permission_config&.dig(:requires)&.present?
88
+
89
+ permission_config[:requires].map { |req| build_full_permission_path(permission, req) }
90
+ end
91
+ end
92
+
93
+ def build_full_permission_path(permission, req)
94
+ return req if req.include?('.')
95
+
96
+ namespace = permission.split('.')[0..-2].join('.')
97
+ "#{namespace}.#{req}"
98
+ end
99
+
100
+ def collect_namespaces(hash, current_path, result)
101
+ hash.each do |key, value|
102
+ if value.is_a?(Hash) && RESERVED_OPTIONS.none? { |opt| value.key?(opt) }
103
+ result << (current_path + [key]).join('.')
104
+ collect_namespaces(value, current_path + [key], result)
105
+ end
106
+ end
107
+ result
108
+ end
109
+
110
+ def flatten_permissions(hash, prefix)
111
+ hash.each_with_object({}) do |(key, value), result|
112
+ next unless value.is_a?(Hash)
113
+
114
+ full_key = prefix.presence ? "#{prefix}.#{key}" : key
115
+ if (RESERVED_OPTIONS & value.keys).present?
116
+ result[full_key] = value
117
+ else
118
+ result.merge!(flatten_permissions(value, full_key))
119
+ end
120
+ end
121
+ end
122
+
123
+ def register_permission(name, options = {})
124
+ options.reverse_merge!(conditions: {}, priority: 0, description: "")
125
+ options[:requires] = Array.wrap(options[:requires]).map(&:to_s) if options[:requires].present?
126
+
127
+ full_path = (@namespace_stack + [name]).join('.')
128
+
129
+ current_hash = @namespace_stack.inject(registered_permissions) do |hash, part|
130
+ hash[part] ||= {}
131
+ end
132
+
133
+ current_hash[name] = options.deep_dup
134
+ registered_permissions[full_path] = options.deep_dup
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DoRole
4
+ class PermissionSetManager
5
+ delegate :clear, to: :@permission_sets
6
+
7
+ def initialize
8
+ @permission_sets = {}.with_indifferent_access
9
+ end
10
+
11
+ def for_namespace(namespace)
12
+ @permission_sets[namespace.to_s] ||= PermissionSet.new
13
+ end
14
+
15
+ def namespaces
16
+ @permission_sets.keys
17
+ end
18
+
19
+ def namespace_exists?(namespace)
20
+ @permission_sets.key?(namespace.to_s)
21
+ end
22
+
23
+ def remove_namespace(namespace)
24
+ @permission_sets.delete(namespace.to_s)
25
+ end
26
+
27
+ def permissions_for(namespace)
28
+ @permission_sets[namespace.to_s].try(:permissions) || {}
29
+ end
30
+
31
+ def valid_permission?(namespace, permission_name)
32
+ @permission_sets[namespace.to_s].try(:valid_permission?, permission_name)
33
+ end
34
+
35
+ def set_inheritance(namespace, inherit_from)
36
+ target_set = for_namespace(namespace)
37
+ Array(inherit_from).each do |parent_namespace|
38
+ if (parent_set = @permission_sets[parent_namespace])
39
+ target_set.registered_permissions.deep_merge!(parent_set.registered_permissions)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DoRole
4
+ class Role < ActiveRecord::Base
5
+ include PermissionManageable
6
+
7
+ has_many :user_roles, dependent: :destroy
8
+ has_many :users, through: :user_roles
9
+ belongs_to :parent_role, class_name: 'DoRole::Role', optional: true
10
+ has_many :child_roles, class_name: 'DoRole::Role', foreign_key: 'parent_role_id'
11
+
12
+ validates :name, presence: true
13
+
14
+ def self.define_role_association(association_name, options = {})
15
+ has_many association_name, options.merge(dependent: :destroy)
16
+ has_many options[:through_model].to_s.pluralize.to_sym, through: association_name if options[:through_model]
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DoRole
4
+ VERSION = "0.2.0"
5
+ end
data/lib/do_role.rb ADDED
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "active_support"
5
+ require "do_role/version"
6
+ require "do_role/conditions_proxy"
7
+ require "do_role/concerns/model"
8
+ require "do_role/concerns/role_model"
9
+ require "do_role/concerns/permission_manageable"
10
+ require "do_role/permission_audit"
11
+ require "do_role/permission_set"
12
+ require "do_role/permission_set_manager"
13
+ require "do_role/ability"
14
+ require "do_role/role"
15
+
16
+ module DoRole
17
+ class Error < StandardError; end
18
+
19
+ class << self
20
+ def permission_set_manager
21
+ @permission_set_manager ||= PermissionSetManager.new
22
+ end
23
+
24
+ def permission_set(namespace = :default, inherit_from: [], &block)
25
+ set = permission_set_manager.for_namespace(namespace)
26
+ if inherit_from.any?
27
+ permission_set_manager.set_inheritance(namespace, inherit_from)
28
+ end
29
+ set.draw(&block) if block_given?
30
+ set
31
+ end
32
+
33
+ def reset_permissions(namespace = :default)
34
+ if namespace == :all
35
+ permission_set_manager.clear
36
+ else
37
+ permission_set_manager.remove_namespace(namespace)
38
+ end
39
+ end
40
+
41
+ def permissions(namespace = :default)
42
+ permission_set_manager.permissions_for(namespace)
43
+ end
44
+
45
+ def valid_permission?(permission_name, namespace = :default)
46
+ permission_set_manager.valid_permission?(namespace, permission_name)
47
+ end
48
+
49
+ def ability_for(user)
50
+ Ability.new(user)
51
+ end
52
+ end
53
+ end
54
+
55
+ # ActiveSupport.on_load(:active_record) do
56
+ # include DoRole::Model
57
+ # end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DoRole
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+
8
+ source_root File.expand_path('templates', __dir__)
9
+
10
+ def self.next_migration_number(dirname)
11
+ if ActiveRecord::Base.timestamped_migrations
12
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
13
+ else
14
+ format("%03d", current_migration_number(dirname) + 1)
15
+ end
16
+ end
17
+
18
+ def create_migration_file
19
+ migration_template(
20
+ 'create_do_role_tables.rb',
21
+ 'db/migrate/create_do_role_tables.rb'
22
+ )
23
+ end
24
+
25
+ def create_initializer
26
+ template 'do_role.rb', 'config/initializers/do_role.rb'
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateDoRoleTables < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :roles do |t|
6
+ t.string :name, null: false
7
+ t.text :permissions
8
+ t.text :description
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :roles, :name, unique: true
14
+
15
+ create_table :user_roles do |t|
16
+ t.references :user, null: false, foreign_key: true
17
+ t.references :role, null: false, foreign_key: true
18
+
19
+ t.timestamps
20
+ end
21
+
22
+ add_index :user_roles, [:user_id, :role_id], unique: true
23
+ end
24
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ DoRole.permission_set do
4
+ # 使用namespace组织权限
5
+ namespace :posts do
6
+ permission :index
7
+ permission :create
8
+ permission :update
9
+ permission :destroy
10
+ end
11
+
12
+ namespace :users do
13
+ permission :manage
14
+ permission :view
15
+ end
16
+
17
+ # 可以添加自定义选项
18
+ namespace :admin do
19
+ permission :dashboard, description: "访问管理后台"
20
+ end
21
+ end
@@ -0,0 +1,24 @@
1
+ <%= form_with model: @role do |f| %>
2
+ <div class="permission-form">
3
+ <% DoRole.permission_set.registered_permissions.group_by { |name, _| name.split('.').first }.each do |namespace, permissions| %>
4
+ <div class="permission-group">
5
+ <h3><%= namespace.titleize %></h3>
6
+ <div class="permission-items">
7
+ <% permissions.each do |permission_name, permission| %>
8
+ <div class="permission-item">
9
+ <%= check_box_tag "role[permissions][]", permission_name, @role.has_permission?(permission_name), id: "permission_#{permission_name.gsub('.', '_')}" %>
10
+ <%= label_tag "permission_#{permission_name.gsub('.', '_')}", permission_name %>
11
+ <% if permission[:description].present? %>
12
+ <small class="permission-description"><%= permission[:description] %></small>
13
+ <% end %>
14
+ </div>
15
+ <% end %>
16
+ </div>
17
+ </div>
18
+ <% end %>
19
+ </div>
20
+
21
+ <div class="form-actions">
22
+ <%= f.submit "保存", class: "btn btn-primary" %>
23
+ </div>
24
+ <% end %>
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: do_role
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - doabit
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-02-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 7.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 7.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 7.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 7.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: activerecord
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 7.0.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 7.0.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: sqlite3
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: DoRole provides a clean and intuitive way to manage roles and permissions
98
+ in Rails applications
99
+ email:
100
+ - doinsist@gmail.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - README.md
106
+ - Rakefile
107
+ - lib/do_role.rb
108
+ - lib/do_role/ability.rb
109
+ - lib/do_role/concerns/model.rb
110
+ - lib/do_role/concerns/permission_manageable.rb
111
+ - lib/do_role/concerns/role_model.rb
112
+ - lib/do_role/conditions_proxy.rb
113
+ - lib/do_role/permission_audit.rb
114
+ - lib/do_role/permission_set.rb
115
+ - lib/do_role/permission_set_manager.rb
116
+ - lib/do_role/role.rb
117
+ - lib/do_role/version.rb
118
+ - lib/generators/do_role/install/install_generator.rb
119
+ - lib/generators/do_role/install/templates/create_do_role_tables.rb
120
+ - lib/generators/do_role/install/templates/do_role.rb
121
+ - lib/generators/do_role/templates/permission_form.html.erb
122
+ homepage: https://github.com/doabit/do_role
123
+ licenses:
124
+ - MIT
125
+ metadata: {}
126
+ post_install_message:
127
+ rdoc_options: []
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ required_rubygems_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ requirements: []
141
+ rubygems_version: 3.5.22
142
+ signing_key:
143
+ specification_version: 4
144
+ summary: A simple and flexible role management solution for Rails applications
145
+ test_files: []