Có một hàm mà lập trình viên Odoo nào cũng gõ trong tuần đầu tiên, và cũng là hàm bị hiểu sai nhiều nhất: sudo(). Cứ gặp AccessError là người ta chấm thêm .sudo() vào, lỗi biến mất, xong việc. Nhưng rất ít người dừng lại hỏi: chính xác thì sudo() vừa làm gì với phiên làm việc của tôi?

Câu trả lời phổ biến — "nó biến tôi thành admin" — là sai. Và hiểu sai chỗ này dẫn thẳng tới lỗ hổng bảo mật. Bài này mổ xẻ sudo() bằng dữ liệu thật trên một Odoo 19 đang chạy.

Dựng một nhân viên bị giới hạn quyền

Để thấy sudo() làm gì, ta cần một user không phải admin. Mình tạo một nhân viên chỉ có nhóm quyền cơ bản (base.group_user) rồi chạy mọi thứ dưới danh nghĩa họ bằng env(user=...):

from odoo.fields import Command
gid = env.ref('base.group_user').id
demo = env['res.users'].create({
    'name': 'NV Han Che',
    'group_ids': [Command.set([gid])],   # Odoo 19: field là group_ids, KHÔNG phải groups_id
})
env.cr.commit()

env_demo = env(user=demo.id)   # từ giờ mọi thao tác chạy như nhân viên này

Một điểm khiến người quen Odoo cũ vấp ngay: trên res.users, field liên kết nhóm quyền trong Odoo 19 tên là group_ids, không còn là groups_id như các phiên bản trước. Gõ tên cũ, bạn nhận ValueError: Invalid field 'groups_id' in 'res.users'. Đây là một trong loạt đổi tên của Odoo 19.

sudo() làm được gì: bỏ qua quyền

Nhân viên cơ bản không có quyền đọc bảng cấu hình máy chủ thư (ir.mail_server). Thử đọc, và thêm sudo() để thấy khác biệt:

Ảnh chụp phiên odoo shell chạy dưới danh nghĩa nhân viên id 8, câu đọc ir.mail_server ném AccessError You are not allowed to access Mail Server, câu thứ hai thêm sudo trả về số đếm 1, phần dưới in ra trước sudo uid 8 user NV Han Che su False và sau sudo uid vẫn 8 user vẫn NV Han Che nhưng su True

Hình 1: Cùng một nhân viên, cùng một bảng. Không có sudo() thì AccessError; thêm sudo() thì đọc được (đếm ra 1 máy chủ thư). Đây là công dụng ai cũng biết. Điều đáng chú ý nằm ở ba dòng cuối — ta sẽ mổ ngay dưới đây.

Tới đây thì đúng như kỳ vọng: sudo() cho phép vượt qua rào access rights. Nhưng câu hỏi thật sự là nó vượt bằng cách nào.

sudo() KHÔNG đổi user — chỉ bật một cờ

Đây là phần cốt lõi. In ra danh tính phiên trước và sau khi gọi sudo():

p = env_demo['res.partner']
print(env_demo.uid, env_demo.user.name, env_demo.su)  # 8  'NV Han Che'  False
ps = p.sudo()
print(ps.env.uid, ps.env.user.name, ps.env.su)        # 8  'NV Han Che'  True

Nhìn kỹ: sau sudo(), uid vẫn là 8, user vẫn là "NV Han Che". User không hề đổi thành admin. Thứ duy nhất đổi là env.su — từ False thành True. Đó là một cờ boolean gọi là superuser mode: khi bật, tầng ORM bỏ qua bước kiểm tra access rights và record rules, nhưng danh tính người dùng thì giữ nguyên.

Điều này đi ngược trực giác của nhiều người (kể cả tài liệu cũ trên mạng, viết từ thời Odoo 8 khi sudo() thật sự chuyển sang SUPERUSER_ID). Trên Odoo 19:

  • SUPERUSER_ID vẫn là 1 — đó là một user riêng (base.user_root), không phải nhân viên của bạn.
  • record.sudo() không biến bạn thành user số 1. Nó giữ bạn là chính bạn, chỉ tạm tắt việc kiểm quyền.

Hệ quả thực tế: những field như create_uid, write_uid, hay các field ghi "ai làm việc này" vẫn ghi đúng nhân viên thật, chứ không bị đóng dấu thành "Administrator". Bạn bỏ qua quyền mà không đánh mất dấu vết ai đã hành động.

Tắt lại: sudo(False)

Superuser mode có thể tắt bằng sudo(False):

ps.sudo(False).env.su   # False — quay lại chế độ kiểm quyền bình thường

Hữu ích khi bạn có một recordset đang ở chế độ su=True (ví dụ nhận từ một hàm khác) và muốn cố tình áp lại quyền của người dùng thật cho những thao tác tiếp theo.

Dùng sudo() cho đúng

Vì sudo() tắt toàn bộ tầng kiểm quyền, nó là con dao hai lưỡi. Vài quy tắc rút ra từ chính cơ chế trên:

def tich_diem_toan_he_thong(self, so_diem):
    self.ensure_one()
    # Kiểm TRƯỚC, sudo SAU — không bao giờ sudo() trên dữ liệu chưa xác thực
    if so_diem < 0:
        raise ValidationError("Điểm không hợp lệ")
    # sudo() cho ĐÚNG thao tác cần, phạm vi hẹp nhất có thể
    self.env['ir.config_parameter'].sudo().set_param('quan_ca_phe.diem_cuoi', so_diem)

Ảnh chụp đoạn mã Python trên nền tối minh hoạ một phương thức dùng sudo đúng cách, gọi ir.config_parameter chấm sudo để ghi một tham số cấu hình, có chú thích kiểm tra điều kiện trước khi sudo, và ba dòng cuối cho thấy env chấm su trả False khi thường, sudo chấm env chấm su trả True, sudo False chấm env chấm su trả về False

Hình 2: Khuôn mẫu dùng sudo() an toàn. Ba điểm: (1) kiểm tra đầu vào trước, sudo() sau — vì sudo() bỏ qua cả record rules, nếu để lọt một domain hay id do người dùng đưa vào mà chưa lọc, bạn cho họ đọc/ghi bản ghi lẽ ra không được phép; (2) sudo() ở phạm vi hẹp nhất, đúng một truy vấn cần đặc quyền, không rải khắp hàm; (3) env.su cho bạn kiểm tra trạng thái hiện tại bất cứ lúc nào.

Ba cạm bẫy thường gặp khi lạm dụng sudo():

  • sudo() rồi thao tác trên id người dùng gửi lên. Record rules là thứ ngăn nhân viên A đọc đơn của nhân viên B. sudo() tắt nó. Nếu bạn self.env['sale.order'].sudo().browse(order_id) với order_id đến thẳng từ request, bạn vừa cho A đọc mọi đơn của mọi người.
  • sudo() cả một khối lớn "cho tiện". Càng nhiều dòng chạy dưới su=True, càng nhiều chỗ vô tình ghi đè dữ liệu vượt quyền. Thu hẹp về đúng truy vấn cần.
  • Tưởng sudo() đổi công ty hay ngữ cảnh. Không. Nó chỉ động tới cờ su. Công ty, ngôn ngữ, timezone vẫn nguyên — đó là việc của with_company và with_context (xem lại bài với ngữ cảnh).

Khi nào thật sự cần sudo()

Chính đáng nhất là khi nghiệp vụ đòi một thao tác vượt quyền của người đang thao tác, và bạn đã tự kiểm soát tính đúng đắn: nhân viên bán hàng tạo đơn cần cập nhật một sequence dùng chung; một portal user gửi form cần ghi vào bảng họ không có quyền trực tiếp nhưng luồng nghiệp vụ cho phép. Trong các ca đó, bạn thay hệ thống làm một việc cụ thể, sau khi đã tự xác thực — chứ không phải "tắt quyền cho nhanh".

Ba ý mang về

  1. sudo() không đổi user thành admin. Nó giữ nguyên uid/user và chỉ bật cờ env.su = True để ORM bỏ qua access rights và record rules. SUPERUSER_ID (1) là user riêng, sudo() không hoá thân thành nó.
  2. Vì sudo() tắt cả tầng record rules, luôn xác thực đầu vào trước, sudo() sau, và giữ phạm vi hẹp nhất — đừng bao giờ sudo() trên id/domain do người dùng đưa vào mà chưa lọc.
  3. Odoo 19 đổi field nhóm quyền trên res.users thành group_ids (không còn groups_id); sudo(False) tắt superuser mode để áp lại quyền thật.

Lần sau ta phân biệt ba anh em hay bị nhầm với sudo(): Phần sau nói về with_user và with_company — đổi ai đang chạy và công ty nào đang áp, khác hẳn việc chỉ tắt kiểm quyền.