Initial commit: Redmine Role Groups Plugin
🤖 Generated with Claude Code
This commit is contained in:
commit
16263fdccb
225
app/controllers/role_groups_controller.rb
Normal file
225
app/controllers/role_groups_controller.rb
Normal file
@ -0,0 +1,225 @@
|
||||
class RoleGroupsController < ApplicationController
|
||||
layout 'admin'
|
||||
before_action :require_admin
|
||||
before_action :find_role_group, only: [:show, :edit, :update, :destroy, :add_member, :remove_member]
|
||||
|
||||
def index
|
||||
@role_groups = RoleGroup.sorted.includes(:users)
|
||||
end
|
||||
|
||||
def show
|
||||
@members = @role_group.role_group_members.includes(:user).order('users.lastname, users.firstname')
|
||||
end
|
||||
|
||||
def new
|
||||
@role_group = RoleGroup.new
|
||||
end
|
||||
|
||||
def create
|
||||
@role_group = RoleGroup.new(role_group_params)
|
||||
if @role_group.save
|
||||
flash[:notice] = l(:notice_successful_create)
|
||||
redirect_to '/role_groups'
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
if @role_group.update(role_group_params)
|
||||
flash[:notice] = l(:notice_successful_update)
|
||||
redirect_to '/role_groups'
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@role_group.destroy
|
||||
flash[:notice] = l(:notice_successful_delete)
|
||||
redirect_to '/role_groups'
|
||||
end
|
||||
|
||||
def add_member
|
||||
user = find_or_create_user_from_ldap(params[:user_id], params[:ldap_uid], params[:ldap_id])
|
||||
|
||||
if user
|
||||
member = @role_group.role_group_members.find_or_initialize_by(user: user)
|
||||
member.note = params[:note]
|
||||
if member.save
|
||||
flash[:notice] = "#{user.name} 추가됨"
|
||||
else
|
||||
flash[:error] = member.errors.full_messages.join(', ')
|
||||
end
|
||||
else
|
||||
flash[:error] = '사용자를 찾을 수 없습니다'
|
||||
end
|
||||
redirect_to role_group_path(@role_group)
|
||||
end
|
||||
|
||||
def remove_member
|
||||
member = @role_group.role_group_members.find_by(user_id: params[:user_id])
|
||||
if member
|
||||
member.destroy
|
||||
flash[:notice] = '멤버 제거됨'
|
||||
end
|
||||
redirect_to role_group_path(@role_group)
|
||||
end
|
||||
|
||||
def search_ldap
|
||||
query = params[:q].to_s.strip
|
||||
if query.length < 2
|
||||
render json: []
|
||||
return
|
||||
end
|
||||
|
||||
results = []
|
||||
|
||||
AuthSourceLdap.all.each do |ldap|
|
||||
begin
|
||||
ldap_users = search_ldap_users(ldap, query)
|
||||
ldap_users.each do |entry|
|
||||
uid = entry[ldap.attr_login]&.first
|
||||
next unless uid
|
||||
|
||||
existing_user = User.find_by(login: uid)
|
||||
|
||||
results << {
|
||||
uid: uid,
|
||||
name: "#{entry[ldap.attr_firstname]&.first} #{entry[ldap.attr_lastname]&.first}".strip,
|
||||
email: entry[ldap.attr_mail]&.first,
|
||||
ldap_id: ldap.id,
|
||||
exists: existing_user.present?,
|
||||
user_id: existing_user&.id
|
||||
}
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error "LDAP search error: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
render json: results.uniq { |r| r[:uid] }.first(20)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_role_group
|
||||
@role_group = RoleGroup.find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
|
||||
def role_group_params
|
||||
params.require(:role_group).permit(:name, :description, :color, :position)
|
||||
end
|
||||
|
||||
def search_ldap_users(ldap, query)
|
||||
options = {
|
||||
host: ldap.host,
|
||||
port: ldap.port,
|
||||
auth: {
|
||||
method: :simple,
|
||||
username: ldap.account,
|
||||
password: ldap.account_password
|
||||
}
|
||||
}
|
||||
|
||||
if ldap.tls
|
||||
if ldap.verify_peer
|
||||
options[:encryption] = { method: :simple_tls }
|
||||
else
|
||||
options[:encryption] = { method: :simple_tls, tls_options: { verify_mode: OpenSSL::SSL::VERIFY_NONE } }
|
||||
end
|
||||
end
|
||||
|
||||
conn = Net::LDAP.new(options)
|
||||
|
||||
filter = Net::LDAP::Filter.begins(ldap.attr_login, query) |
|
||||
Net::LDAP::Filter.begins(ldap.attr_firstname, query) |
|
||||
Net::LDAP::Filter.begins(ldap.attr_lastname, query)
|
||||
|
||||
if ldap.filter.present?
|
||||
base_filter = Net::LDAP::Filter.construct(ldap.filter)
|
||||
filter = base_filter & filter
|
||||
end
|
||||
|
||||
conn.search(
|
||||
base: ldap.base_dn,
|
||||
filter: filter,
|
||||
attributes: [ldap.attr_login, ldap.attr_firstname, ldap.attr_lastname, ldap.attr_mail],
|
||||
size: 20
|
||||
) || []
|
||||
end
|
||||
|
||||
def find_or_create_user_from_ldap(user_id, ldap_uid, ldap_id)
|
||||
return User.find(user_id) if user_id.present?
|
||||
|
||||
return nil if ldap_uid.blank?
|
||||
|
||||
user = User.find_by(login: ldap_uid)
|
||||
return user if user
|
||||
|
||||
ldap = AuthSourceLdap.find_by(id: ldap_id)
|
||||
return nil unless ldap
|
||||
|
||||
user_info = get_ldap_user_info(ldap, ldap_uid)
|
||||
return nil unless user_info
|
||||
|
||||
user = User.new(
|
||||
login: ldap_uid,
|
||||
firstname: user_info[:firstname] || ldap_uid,
|
||||
lastname: user_info[:lastname] || '-',
|
||||
mail: user_info[:mail] || "#{ldap_uid}@example.com",
|
||||
auth_source_id: ldap.id,
|
||||
status: User::STATUS_ACTIVE
|
||||
)
|
||||
user.random_password
|
||||
user.save ? user : nil
|
||||
end
|
||||
|
||||
def get_ldap_user_info(ldap, uid)
|
||||
options = {
|
||||
host: ldap.host,
|
||||
port: ldap.port,
|
||||
auth: {
|
||||
method: :simple,
|
||||
username: ldap.account,
|
||||
password: ldap.account_password
|
||||
}
|
||||
}
|
||||
|
||||
if ldap.tls
|
||||
if ldap.verify_peer
|
||||
options[:encryption] = { method: :simple_tls }
|
||||
else
|
||||
options[:encryption] = { method: :simple_tls, tls_options: { verify_mode: OpenSSL::SSL::VERIFY_NONE } }
|
||||
end
|
||||
end
|
||||
|
||||
conn = Net::LDAP.new(options)
|
||||
|
||||
filter = Net::LDAP::Filter.eq(ldap.attr_login, uid)
|
||||
if ldap.filter.present?
|
||||
base_filter = Net::LDAP::Filter.construct(ldap.filter)
|
||||
filter = base_filter & filter
|
||||
end
|
||||
|
||||
result = conn.search(
|
||||
base: ldap.base_dn,
|
||||
filter: filter,
|
||||
attributes: [ldap.attr_login, ldap.attr_firstname, ldap.attr_lastname, ldap.attr_mail],
|
||||
size: 1
|
||||
)&.first
|
||||
|
||||
return nil unless result
|
||||
|
||||
{
|
||||
firstname: result[ldap.attr_firstname]&.first,
|
||||
lastname: result[ldap.attr_lastname]&.first,
|
||||
mail: result[ldap.attr_mail]&.first
|
||||
}
|
||||
end
|
||||
end
|
||||
12
app/models/role_group.rb
Normal file
12
app/models/role_group.rb
Normal file
@ -0,0 +1,12 @@
|
||||
class RoleGroup < ActiveRecord::Base
|
||||
has_many :role_group_members, dependent: :destroy
|
||||
has_many :users, through: :role_group_members
|
||||
|
||||
validates :name, presence: true, uniqueness: true
|
||||
|
||||
scope :sorted, -> { order(:position, :name) }
|
||||
|
||||
def member_count
|
||||
users.count
|
||||
end
|
||||
end
|
||||
7
app/models/role_group_member.rb
Normal file
7
app/models/role_group_member.rb
Normal file
@ -0,0 +1,7 @@
|
||||
class RoleGroupMember < ActiveRecord::Base
|
||||
belongs_to :role_group
|
||||
belongs_to :user
|
||||
|
||||
validates :role_group_id, presence: true
|
||||
validates :user_id, presence: true, uniqueness: { scope: :role_group_id }
|
||||
end
|
||||
23
app/views/role_groups/_form.html.erb
Normal file
23
app/views/role_groups/_form.html.erb
Normal file
@ -0,0 +1,23 @@
|
||||
<fieldset class="box tabular">
|
||||
<legend>역할 그룹 정보</legend>
|
||||
|
||||
<p>
|
||||
<%= f.label :name, '그룹명' %> <span class="required">*</span>
|
||||
<%= f.text_field :name, size: 40, required: true, placeholder: 'DBA, 시스템, 방화벽 등' %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= f.label :description, '설명' %>
|
||||
<%= f.text_area :description, rows: 3, cols: 60, placeholder: '이 역할 그룹에 대한 설명' %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= f.label :color, '색상' %>
|
||||
<%= f.color_field :color, value: (@role_group.color || '#007bff') %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= f.label :position, '순서' %>
|
||||
<%= f.number_field :position, value: (@role_group.position || 0), min: 0 %>
|
||||
</p>
|
||||
</fieldset>
|
||||
9
app/views/role_groups/edit.html.erb
Normal file
9
app/views/role_groups/edit.html.erb
Normal file
@ -0,0 +1,9 @@
|
||||
<h2>역할 그룹 수정: <%= @role_group.name %></h2>
|
||||
|
||||
<%= form_for @role_group, url: update_role_group_path(@role_group), method: :patch, html: { class: 'tabular' } do |f| %>
|
||||
<%= render partial: 'form', locals: { f: f } %>
|
||||
<p>
|
||||
<%= f.submit '저장', class: 'button-positive' %>
|
||||
<%= link_to '취소', '/role_groups', class: 'button' %>
|
||||
</p>
|
||||
<% end %>
|
||||
38
app/views/role_groups/index.html.erb
Normal file
38
app/views/role_groups/index.html.erb
Normal file
@ -0,0 +1,38 @@
|
||||
<h2>역할 그룹 관리</h2>
|
||||
|
||||
<p>
|
||||
<%= link_to '새 역할 그룹', new_role_group_path, class: 'icon icon-add' %>
|
||||
</p>
|
||||
|
||||
<% if @role_groups.any? %>
|
||||
<table class="list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>역할 그룹</th>
|
||||
<th>설명</th>
|
||||
<th>멤버 수</th>
|
||||
<th>작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @role_groups.each do |group| %>
|
||||
<tr>
|
||||
<td>
|
||||
<span style="display: inline-block; width: 12px; height: 12px; background: <%= group.color %>; border-radius: 2px; margin-right: 8px;"></span>
|
||||
<strong><%= link_to group.name, role_group_path(group) %></strong>
|
||||
</td>
|
||||
<td><%= truncate(group.description, length: 50) %></td>
|
||||
<td><%= group.member_count %>명</td>
|
||||
<td>
|
||||
<%= link_to '관리', role_group_path(group), class: 'icon icon-user' %>
|
||||
<%= link_to '수정', edit_role_group_path(group), class: 'icon icon-edit' %>
|
||||
<%= link_to '삭제', destroy_role_group_path(group), method: :delete,
|
||||
data: { confirm: '정말 삭제하시겠습니까?' }, class: 'icon icon-del' %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p class="nodata">등록된 역할 그룹이 없습니다.</p>
|
||||
<% end %>
|
||||
9
app/views/role_groups/new.html.erb
Normal file
9
app/views/role_groups/new.html.erb
Normal file
@ -0,0 +1,9 @@
|
||||
<h2>새 역할 그룹</h2>
|
||||
|
||||
<%= form_for @role_group, url: '/role_groups/create', html: { class: 'tabular' } do |f| %>
|
||||
<%= render partial: 'form', locals: { f: f } %>
|
||||
<p>
|
||||
<%= f.submit '생성', class: 'button-positive' %>
|
||||
<%= link_to '취소', '/role_groups', class: 'button' %>
|
||||
</p>
|
||||
<% end %>
|
||||
125
app/views/role_groups/show.html.erb
Normal file
125
app/views/role_groups/show.html.erb
Normal file
@ -0,0 +1,125 @@
|
||||
<h2>
|
||||
<span style="display: inline-block; width: 16px; height: 16px; background: <%= @role_group.color %>; border-radius: 3px; margin-right: 10px; vertical-align: middle;"></span>
|
||||
<%= @role_group.name %>
|
||||
</h2>
|
||||
|
||||
<% if @role_group.description.present? %>
|
||||
<p><%= @role_group.description %></p>
|
||||
<% end %>
|
||||
|
||||
<hr />
|
||||
|
||||
<h3>멤버 (<%= @members.count %>명)</h3>
|
||||
|
||||
<% if @members.any? %>
|
||||
<table class="list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>이름</th>
|
||||
<th>ID</th>
|
||||
<th>이메일</th>
|
||||
<th>비고</th>
|
||||
<th>작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @members.each do |member| %>
|
||||
<tr>
|
||||
<td><%= link_to member.user.name, user_path(member.user) %></td>
|
||||
<td><%= member.user.login %></td>
|
||||
<td><%= member.user.mail %></td>
|
||||
<td><%= member.note %></td>
|
||||
<td>
|
||||
<%= link_to '제거', remove_member_role_group_path(@role_group, user_id: member.user_id),
|
||||
method: :delete, data: { confirm: '이 멤버를 제거하시겠습니까?' }, class: 'icon icon-del' %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p class="nodata">멤버가 없습니다.</p>
|
||||
<% end %>
|
||||
|
||||
<hr />
|
||||
|
||||
<h3>멤버 추가 (LDAP 검색)</h3>
|
||||
|
||||
<div class="box">
|
||||
<p>
|
||||
<label for="ldap_search">사용자 검색:</label>
|
||||
<input type="text" id="ldap_search" size="30" placeholder="이름 또는 ID로 검색..." autocomplete="off" />
|
||||
<span id="search_status" style="margin-left: 10px; color: #666;"></span>
|
||||
</p>
|
||||
<div id="search_results" style="margin-top: 10px;"></div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<p>
|
||||
<%= link_to '목록으로', '/role_groups', class: 'icon icon-back' %>
|
||||
<%= link_to '수정', edit_role_group_path(@role_group), class: 'icon icon-edit' %>
|
||||
</p>
|
||||
|
||||
<%= javascript_tag do %>
|
||||
var searchTimeout = null;
|
||||
|
||||
$('#ldap_search').on('keyup', function() {
|
||||
var query = $(this).val();
|
||||
|
||||
if (searchTimeout) clearTimeout(searchTimeout);
|
||||
|
||||
if (query.length < 2) {
|
||||
$('#search_results').html('');
|
||||
$('#search_status').text('');
|
||||
return;
|
||||
}
|
||||
|
||||
$('#search_status').text('검색 중...');
|
||||
|
||||
searchTimeout = setTimeout(function() {
|
||||
$.ajax({
|
||||
url: '<%= search_ldap_role_groups_path %>',
|
||||
data: { q: query },
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
$('#search_status').text(data.length + '명 발견');
|
||||
var html = '';
|
||||
|
||||
if (data.length === 0) {
|
||||
html = '<p style="color: #999;">검색 결과가 없습니다.</p>';
|
||||
} else {
|
||||
html = '<table class="list" style="width: auto;"><thead><tr><th>ID</th><th>이름</th><th>이메일</th><th>상태</th><th></th></tr></thead><tbody>';
|
||||
data.forEach(function(user) {
|
||||
html += '<tr>';
|
||||
html += '<td>' + user.uid + '</td>';
|
||||
html += '<td>' + user.name + '</td>';
|
||||
html += '<td>' + (user.email || '-') + '</td>';
|
||||
html += '<td>' + (user.exists ? '<span style="color: green;">등록됨</span>' : '<span style="color: blue;">LDAP</span>') + '</td>';
|
||||
html += '<td>';
|
||||
html += '<form action="<%= add_member_role_group_path(@role_group) %>" method="post" style="display:inline;">';
|
||||
html += '<input type="hidden" name="authenticity_token" value="' + $('meta[name="csrf-token"]').attr('content') + '">';
|
||||
if (user.exists) {
|
||||
html += '<input type="hidden" name="user_id" value="' + user.user_id + '">';
|
||||
} else {
|
||||
html += '<input type="hidden" name="ldap_uid" value="' + user.uid + '">';
|
||||
html += '<input type="hidden" name="ldap_id" value="' + user.ldap_id + '">';
|
||||
}
|
||||
html += '<input type="submit" value="추가" class="button-positive" style="padding: 2px 10px;">';
|
||||
html += '</form>';
|
||||
html += '</td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
}
|
||||
|
||||
$('#search_results').html(html);
|
||||
},
|
||||
error: function() {
|
||||
$('#search_status').text('검색 오류');
|
||||
$('#search_results').html('<p style="color: red;">검색 중 오류가 발생했습니다.</p>');
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
<% end %>
|
||||
12
config/routes.rb
Normal file
12
config/routes.rb
Normal file
@ -0,0 +1,12 @@
|
||||
Rails.application.routes.draw do
|
||||
get 'role_groups', to: 'role_groups#index'
|
||||
get 'role_groups/new', to: 'role_groups#new', as: 'new_role_group'
|
||||
get 'role_groups/search_ldap', to: 'role_groups#search_ldap', as: 'search_ldap_role_groups'
|
||||
post 'role_groups/create', to: 'role_groups#create'
|
||||
get 'role_groups/:id', to: 'role_groups#show', as: 'role_group'
|
||||
get 'role_groups/:id/edit', to: 'role_groups#edit', as: 'edit_role_group'
|
||||
patch 'role_groups/:id', to: 'role_groups#update', as: 'update_role_group'
|
||||
delete 'role_groups/:id', to: 'role_groups#destroy', as: 'destroy_role_group'
|
||||
post 'role_groups/:id/add_member', to: 'role_groups#add_member', as: 'add_member_role_group'
|
||||
delete 'role_groups/:id/remove_member/:user_id', to: 'role_groups#remove_member', as: 'remove_member_role_group'
|
||||
end
|
||||
13
db/migrate/001_create_role_groups.rb
Normal file
13
db/migrate/001_create_role_groups.rb
Normal file
@ -0,0 +1,13 @@
|
||||
class CreateRoleGroups < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
create_table :role_groups do |t|
|
||||
t.string :name, null: false
|
||||
t.text :description
|
||||
t.string :color, default: '#007bff'
|
||||
t.integer :position, default: 0
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :role_groups, :name, unique: true
|
||||
end
|
||||
end
|
||||
16
db/migrate/002_create_role_group_members.rb
Normal file
16
db/migrate/002_create_role_group_members.rb
Normal file
@ -0,0 +1,16 @@
|
||||
class CreateRoleGroupMembers < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
create_table :role_group_members do |t|
|
||||
t.bigint :role_group_id, null: false
|
||||
t.integer :user_id, null: false
|
||||
t.text :note
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :role_group_members, :role_group_id
|
||||
add_index :role_group_members, :user_id
|
||||
add_index :role_group_members, [:role_group_id, :user_id], unique: true
|
||||
add_foreign_key :role_group_members, :role_groups
|
||||
add_foreign_key :role_group_members, :users
|
||||
end
|
||||
end
|
||||
9
init.rb
Normal file
9
init.rb
Normal file
@ -0,0 +1,9 @@
|
||||
Redmine::Plugin.register :role_groups do
|
||||
name '역할 그룹 관리'
|
||||
author 'Admin'
|
||||
description 'Role-based grouping for users (DBA, System, Firewall, etc.)'
|
||||
version '1.0.0'
|
||||
|
||||
menu :admin_menu, :role_groups, { controller: 'role_groups', action: 'index' },
|
||||
caption: '역할 그룹', html: { class: 'icon icon-group' }
|
||||
end
|
||||
Loading…
Reference in New Issue
Block a user