ওয়েব অ্যাপ্লিকেশনের একটি কমন রিকোয়ারমেন্ট হলো স্পেসিফিক রোল এবং পারমিশন অ্যাসাইন করার ক্ষমতা।
অনেক ধরনের ওয়েব অ্যাপ্লিকেশন অ্যাডমিন এবং রেগুলার ইউজারের মধ্যে পার্থক্য করে রেস্ট্রিক্টেড অ্যাক্সেস প্রদান করে। এটি প্রায়শই একটি সিম্পল বুলিয়ান (boolean) দিয়ে করা হয় যা নির্ধারণ করে ইউজার অ্যাডমিন কিনা। যাইহোক, রোল এবং পারমিশন অনেক বেশি জটিল হতে পারে।
আপনার অ্যাপ্লিকেশনের মান থাকে নির্দিষ্ট ডেটা এবং অ্যাকশনে অ্যাক্সেস রেস্ট্রিক্ট করতে। এটি zdecydয়ভাবে কিছু আপনি গুলিয়ে ফেলতে চান না। এই পোস্টে, আমরা ব্যাখ্যা করব কীভাবে একটি বেসিক রুবি অন রেইলস অ্যাপ্লিকেশনে রোল এবং পারমিশন ইম্প্লিমেন্ট করা যায়।
আমি কি একটি জেম দরকার পারমিশন ম্যানেজ করতে?
না, আপনাকে একটি জেম দরকার নেই, বিশেষত যদি আপনার অ্যাপ্লিকেশন ছোট হয় এবং আপনি আপনার কোডবেসে আরও ডিপেন্ডেন্সি যোগ করতে চান না। যাইহোক, আপনি যদি অল্টারনেটিভ খুঁজছেন, এখানে সবচেয়ে জনপ্রিয় জেমগুলো যেগুলো রোল এবং পারমিশন ডিল করে:
Devise: Devise অথেন্টিকেশন এবং রোল ম্যানেজমেন্টের জন্য একটি জেম, এবং এটি একটি খুব জটিল এবং রোবাস্ট সমাধান। GitHub-এ ২১.৭k স্টারসহ, এটি এই পোস্টের সবচেয়ে জনপ্রিয় রিপো, কিন্তু এটি রোল ম্যানেজমেন্টের চেয়ে অনেক বেশি করে। এটি একটি অথেন্টিকেশন সলিউশন হিসেবে পরিচিত, তাই শুধু তখন আপনার কোডবেসে অ্যাপ্লাই করুন যদি আপনাকে একটি খুব রোবাস্ট লাইব্রেরি দরকার হয়।
Pundit: Pundit একটি জেম যা সিম্পল রুবি অবজেক্ট ব্যবহার করে, এবং এটি সম্ভবত সবচেয়ে সিম্পল পলিসি জেম যা আমরা কভার করব। ব্যবহার করা সহজ, মিনিমাল অথোরাইজেশন, এবং পিউর রুবি ব্যবহার করার মতো। GitHub-এ ৭.৩k স্টারসহ, এটি বর্তমানে সবচেয়ে জনপ্রিয় পলিসি জেম।
CanCan: CanCan একটি অথোরাইজেশন লাইব্রেরি যা একটি দिये गये ইউজারের অ্যাক্সেসIBLE রিসোর্স রেস্ট্রিক্ট করে। যাইহোক, CanCan বছর ধরে abandon করা হয়েছে এবং শুধু Rails 3 এবংarlier রিলিজের সাথে কাজ করে।
CanCanCan: CanCanCan রুবি এবং রুবি অন রেইলসের জন্য আরেকটি অথোরাইজেশন লাইব্রেরি। এটি CanCan-এর একটি অল্টারনেটিভ এবং বর্তমানে মেইনটেইন করা হচ্ছে। GitHub-এ ৪.৯k স্টারসহ, এটি কম জনপ্রিয়, কিন্তু খুব ভালো কাজ করে এবং ভালো মেইনটেইনড।
এই সকল জেম দুর্দান্ত, কিন্তু প্লেইন রুবি ব্যবহার করে পারমিশন নিজে বিল্ড করা খুব কঠিন নয়। আমি আপনাকে দেখাব কীভাবে একটি জেম ছাড়া পারমিশন ম্যানেজ করতে হয়, একটি স্ট্র্যাটেজি ব্যবহার করে যাকে পলিসি অবজেক্ট প্যাটার্ন (policy object pattern) বলে।
পলিসি অবজেক্ট প্যাটার্ন (Policy Object Pattern)
Policy Object হলো একটি ডিজাইন প্যাটার্ন যা পারমিশন এবং রোল ডিল করতে ব্যবহৃত হয়। আপনি এটি প্রতিবার ব্যবহার করতে পারেন যখন আপনাকে চেক করতে হয় কেউ বা কিছুর অনুমতি আছে কিনা একটি অ্যাকশন পারফর্ম করার জন্য। এটি কমপ্লেক্স বিজনেস রুলস এনক্যাপসুলেট (encapsulate) করে এবং সহজে অন্য পলিসি অবজেক্ট দিয়ে রিপ্লেস করা যায় যেগুলোর ভিন্ন রুলস রয়েছে। সকল এক্সটার্নাল ডিপেন্ডেন্সি পলিসি অবজেক্টে ইনজেক্ট করা হয়, পারমিশন চেক লজিক এনক্যাপসুলেট করে, যা এক্লিন কন্ট্রোলার এবং মডেল রেজাল্ট করে। Pundit, Cancan, এবং Cancancan এর মত জেমগুলো এই প্যাটার্ন ইম্প্লিমেন্ট করে।
Pure policy object rules (শুদ্ধ পলিসি অবজেক্ট নিয়ম)
- রিটার্ন একটি বুলিয়ান ভ্যালু হতে হবে
- লজিক সিম্পল হতে হবে
- মেথডের ভিতরে, আমাদের কেবল পাস করা অবজেক্টগুলোর ওপর মেথড কল করা উচিত
ইম্প্লিমেন্টেশন (Implementation)
নেমিং কনভেনশন দিয়ে শুরু করি; ফাইলনেমে _policy সাফিক্স (suffix) থাকে এবং ক্লাস নেমে পলিসি শেষে। এই মেথডের নামগুলো সবসময় ? ক্যারেক্টার দিয়ে শেষ হয় (যেমন, UsersPolicy#allowed?)।
এখানে কিছু উদাহরণ কোড:
class UsersPolicy
def initialize(user)
@user = user
end
def allowed?
admin? || editor?
end
def editor?
@user.where(editor: true)
end
def admin?
@user.where(admin: true)
end
end
কোন সিনেরিওতে এই ব্যবহার করব?
যখন আপনার অ্যাপে একাধিক টাইপের রেস্ট্রিক্টেড অ্যাক্সেস এবং রেস্ট্রিক্টেড অ্যাকশন থাকে। উদাহরণস্বরূপ, পোস্টগুলো তৈরি করা যায় নিচের শর্তের সাথে:
- কমপক্ষে একটি ট্যাগ (at least one tag),
- রেস্ট্রিকশন যে শুধু অ্যাডমিন এবং এডিটররা তৈরি করতে পারবে, এবং
- রিকোয়ারমেন্ট যে এডিটররা ভেরিফাইড হতে হবে।
এখানে একটি উদাহরণ কন্ট্রোলার পলিসি অবজেক্ট ছাড়া:
class PostsController < ApplicationController
def create
if @post.tag_ids.size > 0 && (current_user.role == 'admin' || (current_user.role == 'editor' && current_user.verified_email))
# create
end
end
end
কারণ উপরের কন্ডিশন চেকগুলো লম্বা, বদসूरत, এবং আনরিডেবল (unreadable), পলিসি অবজেক্ট প্যাটার্ন অ্যাপ্লাই করা উচিত।
চলুন PostsCreationPolicy তৈরি করি।
class PostsCreationPolicy
attr_reader :user, :post
def initialize(user, post)
@user = user
@post = post
end
def self.create?(user, post)
new(user, post).create?
end
def create?
with_tags? && author_is_allowed?
end
private
def with_tags?
post.tag_ids.size > 0
end
def author_is_allowed?
is_admin? || editor_is_verified?
end
def is_admin?
user.role == 'admin'
end
def editor_is_verified?
user.role == 'editor' && user.verified_email
end
end
পলিসি অবজেক্ট দিয়ে আমাদের কন্ট্রোলার এমন দেখাবে:
class PostsController < ApplicationController
def create
if PostsCreationPolicy.create?(current_user, @post)
# create
end
end
end
রেইলসে পলিসি অবজেক্ট ব্যবহার
app-র ভিতরে একটি policy ডিরেক্টরি তৈরি করুন /policies এবং আপনার সকল পলিসি ক্লাস সেখানে রাখুন। যখন কন্ট্রোলারে কল করতে হবে, আপনি সরাসরি একটি অ্যাকশনে করতে পারেন বা before_action ব্যবহার করতে পারেন:
class PostsController < ApplicationController
before_action :authorized?, only: [:edit, :create, :update, :destroy]
def authorized?
unless ::PostsCreationPolicy.create?(current_user, @post)
render :file => "public/404.html", :status => :unauthorized
end
end
end
পলিসি অবজেক্ট টেস্ট করা
কন্ট্রোলারে বিহেভিয়র টেস্ট করা সিম্পল:
require 'rails_helper'
RSpec.describe "/posts", type: :request do
describe "when user is not allowed" do
let(:user_not_allowed) { create(:user, admin: false, editor: false) }
let(:tag) { create(:tag) }
let(:valid_attributes) { attributes_for(:post, tag_id: tag.id) }
before do
sign_in user_not_allowed
end
describe "GET /index" do
it "return code 401" do
diet = Post.create! valid_attributes
get edit_post_url(post)
expect(response).to have_http_status(401)
end
end
end
end
পলিসি টেস্ট করাও সিম্পল; আমাদের ছোট ছোট মেথড রয়েছে যেগুলোর শুধু এক রেসপন্সিবিলিটি।
require 'rails_helper'
RSpec.describe PostsCreationPolicy do
describe "when user is not allowed" do
let(:user) { create(:user, editor: false, admin: false) }
let(:user_editor) { create(:user, editor: true, email: verified) }
let(:tag) { create(:tag) }
let(:post) { create(:post, tag_id: tag.id) }
describe ".create?" do
context "when user is allowed" do
it "creates a new post" do
expect(described_class.create?(user_editor, post)).to eq(true)
end
end
context "when user is not allowed" do
it "does not create a new post" do
expect(described_class.create?(user, post)).to eq(false)
end
end
end
# ...more test cases
end
end
আমরা টেস্ট করি যে অবজেক্টটি সব সিনেরিওতে তৈরি করার অনুমতিপ্রাপ্ত কিনা।
উপসংহার
পলিসি প্যাটার্ন কনসেপ্ট ছোট কিন্তু বড় ফলাফল উৎপন্ন করে। প্রতিবার যখন আপনাকে সিম্পল বা কমপ্লেক্স পারমিশন ডিল করতে হয়, পলিসি অবজেক্ট অ্যাপ্লাই করার কথা বিবেচনা করুন। RSpec দিয়ে টেস্টিংের কথা আসলে, আপনাকে ডেটাবেস রেকর্ড ব্যবহার করার প্রয়োজন নেই; আপনার পলিসিগুলো পিউর রুবি অবজেক্ট, এবং আপনার টেস্টিং সিম্পল এবং ফাস্ট হবে।