PostgreSQL
 sql >> Cơ Sở Dữ Liệu >  >> RDS >> PostgreSQL

Tự cấp phép tài khoản người dùng trong PostgreSQL thông qua quyền truy cập ẩn danh không đặc quyền

Lưu ý từ Somenines:Blog này đang được đăng sau khi Berend Tober qua đời vào ngày 16 tháng 7 năm 2018. Chúng tôi tôn vinh những đóng góp của anh ấy cho cộng đồng PostgreSQL và cầu chúc bình an cho người bạn và người viết khách của chúng tôi.

Trong bài viết trước, chúng tôi đã giới thiệu khái niệm cơ bản về trình kích hoạt PostgreSQL và các hàm được lưu trữ và cung cấp sáu trường hợp sử dụng ví dụ bao gồm xác thực dữ liệu, ghi nhật ký thay đổi, lấy giá trị từ dữ liệu được chèn, ẩn dữ liệu với các chế độ xem có thể cập nhật đơn giản, duy trì dữ liệu tóm tắt trong các bảng riêng biệt và gọi mã an toàn ở đặc quyền nâng cao. Bài viết này xây dựng thêm trên nền tảng đó và trình bày một kỹ thuật sử dụng trình kích hoạt và chức năng được lưu trữ để tạo điều kiện thuận lợi cho việc ủy ​​quyền cấp phép thông tin đăng nhập cho các vai trò có đặc quyền hạn chế (tức là không phải người dùng cấp trên). Tính năng này có thể được sử dụng để giảm bớt khối lượng công việc hành chính cho nhân viên quản trị hệ thống có giá trị cao. Ở mức độ cao nhất, chúng tôi chứng minh người dùng cuối ẩn danh tự cung cấp thông tin đăng nhập, tức là cho phép người dùng cơ sở dữ liệu tiềm năng tự cung cấp thông tin đăng nhập bằng cách triển khai “dynamic SQL” bên trong một hàm được lưu trữ được thực thi ở cấp đặc quyền phạm vi phù hợp. / P>

Đọc thông tin cơ bản hữu ích

Bài viết gần đây của Sebastian Insausti về Cách bảo mật cơ sở dữ liệu PostgreSQL của bạn bao gồm một số mẹo có liên quan cao mà bạn nên quen thuộc, đó là Mẹo # 1 - # 5 về Kiểm soát xác thực máy khách, Cấu hình máy chủ, Quản lý vai trò và người dùng, Quản lý siêu người dùng, và Mã hóa dữ liệu. Chúng tôi sẽ sử dụng các phần của từng mẹo trong bài viết này.

Một bài viết gần đây khác của Joshua Otwell trên PostgreSQL Privileges &User Management cũng có cách xử lý tốt về cấu hình máy chủ và đặc quyền người dùng, đi sâu hơn một chút về hai chủ đề đó.

Bảo vệ lưu lượng mạng

Tính năng được đề xuất liên quan đến việc cho phép người dùng cung cấp thông tin đăng nhập cơ sở dữ liệu và trong khi làm như vậy, họ sẽ chỉ định tên đăng nhập và mật khẩu mới của họ qua mạng. Bảo vệ giao tiếp mạng này là điều cần thiết và có thể đạt được bằng cách cấu hình máy chủ PostgreSQL để hỗ trợ và yêu cầu các kết nối được mã hóa. Bảo mật lớp truyền tải được bật trong tệp postgresql.conf bằng cài đặt “ssl”:

ssl = on

Kiểm soát truy cập dựa trên máy chủ

Đối với trường hợp hiện tại, chúng tôi sẽ thêm một dòng cấu hình truy cập dựa trên máy chủ lưu trữ trong tệp pg_hba.conf cho phép ẩn danh, tức là, đáng tin cậy, đăng nhập vào cơ sở dữ liệu từ một số mạng con thích hợp cho dân số người dùng cơ sở dữ liệu tiềm năng theo nghĩa đen bằng cách sử dụng tên người dùng "Ẩn danh" và dòng cấu hình thứ hai yêu cầu đăng nhập bằng mật khẩu cho bất kỳ tên đăng nhập nào khác. Hãy nhớ rằng cấu hình máy chủ lưu trữ gọi kết quả phù hợp đầu tiên, vì vậy dòng đầu tiên sẽ áp dụng bất cứ khi nào tên người dùng "ẩn danh" được chỉ định, cho phép kết nối đáng tin cậy (tức là không cần mật khẩu) và sau đó bất cứ khi nào tên người dùng khác được chỉ định, mật khẩu sẽ được yêu cầu. Ví dụ:nếu cơ sở dữ liệu mẫu “sackedb” chỉ được sử dụng bởi nhân viên và nội bộ cho các cơ sở của công ty, thì chúng tôi có thể định cấu hình quyền truy cập đáng tin cậy cho một số mạng con nội bộ không thể định tuyến với:

# TYPE  DATABASE USER      ADDRESS        METHOD
hostssl sampledb anonymous 192.168.1.0/24 trust
hostssl sampledb all       192.168.1.0/24 md5

Nếu cơ sở dữ liệu được cung cấp chung cho công chúng, thì chúng tôi có thể định cấu hình quyền truy cập “bất kỳ địa chỉ nào”:

# TYPE  DATABASE USER       ADDRESS  METHOD
hostssl sampledb anonymous  all      trust
hostssl sampledb all        all      md5

Lưu ý rằng điều trên có thể nguy hiểm nếu không có các biện pháp phòng ngừa bổ sung, có thể trong thiết kế ứng dụng hoặc thiết bị tường lửa, để giới hạn tốc độ sử dụng tính năng này, bởi vì bạn biết một số script kiddie sẽ tự động tạo tài khoản vô tận chỉ dành cho lulz.

Cũng lưu ý rằng chúng tôi đã chỉ định loại kết nối là “hostssl” có nghĩa là các kết nối được thực hiện bằng TCP / IP chỉ thành công khi kết nối được thực hiện bằng mã hóa SSL để bảo vệ lưu lượng mạng khỏi bị nghe trộm.

Khóa giản đồ công khai

Vì chúng tôi đang cho phép những người có thể không xác định (tức là không đáng tin cậy) truy cập vào cơ sở dữ liệu, chúng tôi sẽ muốn đảm bảo rằng khả năng truy cập mặc định bị giới hạn. Một biện pháp quan trọng là thu hồi đặc quyền tạo đối tượng lược đồ công khai mặc định để giảm thiểu lỗ hổng PostgreSQL được xuất bản gần đây liên quan đến đặc quyền lược đồ mặc định (xem Thực sự khóa lược đồ công khai của bạn).

Cơ sở dữ liệu mẫu

Chúng tôi sẽ bắt đầu với một cơ sở dữ liệu mẫu trống cho mục đích minh họa:

create database sampledb;
\connect sampledb

revoke create on schema public from public;
alter default privileges revoke all privileges on tables from public;

Chúng tôi cũng tạo vai trò đăng nhập ẩn danh tương ứng với cài đặt pg_hba.conf trước đó.

create role anonymous login
    nosuperuser 
    noinherit 
    nocreatedb 
    nocreaterole 
    Noreplication;

Và sau đó chúng tôi làm một cái gì đó mới lạ bằng cách xác định một cái nhìn độc đáo:

create or replace view person as 
 select 
    null::name as login_name,
    null::name as login_pass;

Chế độ xem này không tham chiếu đến bảng và do đó, một truy vấn chọn luôn trả về một hàng trống:

select * from person;
 login_name | login_pass 
------------+-------------
            | 
(1 row)

Một điều mà điều này làm được đối với chúng tôi là cung cấp tài liệu hoặc gợi ý cho người dùng cuối về dữ liệu nào cần thiết để thiết lập tài khoản. Nghĩa là, bằng cách truy vấn bảng, ngay cả khi kết quả là một hàng trống, kết quả sẽ hiển thị tên của hai phần tử dữ liệu.

Nhưng tốt hơn nữa, sự tồn tại của chế độ xem này cho phép xác định các kiểu dữ liệu được yêu cầu:

\d person
      View "public.person"
    Column    | Type | Modifiers 
--------------+------+-----------
 login_name   | name | 
 login_pass   | name | 

Chúng tôi sẽ triển khai chức năng cung cấp thông tin xác thực với một hàm và trình kích hoạt được lưu trữ, vì vậy hãy khai báo một mẫu hàm trống và trình kích hoạt được liên kết:

create or replace function person_iit()
  returns trigger
  set schema 'public'
  language plpgsql
  security definer
  as '
  begin
  end;
  ';

create trigger person_iit
  instead of insert
  on person
  for each row execute procedure person_iit();

Lưu ý rằng chúng tôi đang tuân theo quy ước đặt tên được đề xuất từ ​​bài viết trước, sử dụng tên bảng được liên kết với hậu tố viết tắt ngắn gọn biểu thị các thuộc tính của mối quan hệ kích hoạt giữa bảng và hàm được lưu trữ cho kích hoạt INSTEAD OF INSERT (tức là hậu tố “ iit ”). Chúng tôi cũng đã thêm vào hàm được lưu trữ các thuộc tính SCHEMA và SECURITY DEFINER:cái trước vì cách đặt đường dẫn tìm kiếm áp dụng cho thời gian thực thi hàm là một cách tốt, và cái sau để tạo điều kiện cho việc tạo vai trò, thường là quyền cấp trên cơ sở dữ liệu. nhưng trong trường hợp này sẽ được ủy quyền cho người dùng ẩn danh.

Và cuối cùng, chúng tôi thêm các quyền vừa đủ tối thiểu trên chế độ xem để truy vấn và chèn:

grant select, insert on table person to anonymous;
Tải xuống Báo cáo chính thức hôm nay Quản lý &Tự động hóa PostgreSQL với ClusterControlTìm hiểu về những điều bạn cần biết để triển khai, giám sát, quản lý và mở rộng PostgreSQLTải xuống Báo cáo chính thức

Hãy xem lại

Trước khi triển khai mã chức năng được lưu trữ, hãy xem lại những gì chúng tôi có. Đầu tiên là cơ sở dữ liệu mẫu do người dùng postgres sở hữu:

\l
                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges   
-----------+----------+----------+-------------+-------------+-----------------------
 sampledb  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | 
And there’s the user roles, including the database superuser and the newly-created anonymous login roles:
\du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 anonymous | No inheritance                                             | {}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Và có chế độ xem chúng tôi đã tạo và danh sách các đặc quyền truy cập tạo và đọc được người dùng postgres cấp cho người dùng ẩn danh:

\d
         List of relations
 Schema |  Name  | Type |  Owner   
--------+--------+------+----------
 public | person | view | postgres
(1 row)


\dp
                                Access privileges
 Schema |  Name  | Type |     Access privileges     | Column privileges | Policies 
--------+--------+------+---------------------------+-------------------+----------
 public | person | view | postgres=arwdDxt/postgres+|                   | 
        |        |      | anonymous=ar/postgres     |                   | 
(1 row)

Cuối cùng, chi tiết bảng hiển thị tên cột và kiểu dữ liệu cũng như trình kích hoạt được liên kết:

\d person
      View "public.person"
    Column    | Type | Modifiers 
--------------+------+-----------
 login_name   | name | 
 login_pass   | name | 
Triggers:
    person_iit INSTEAD OF INSERT ON person FOR EACH ROW EXECUTE PROCEDURE person_iit()

SQL động

Chúng tôi sẽ sử dụng SQL động, tức là, xây dựng dạng cuối cùng của câu lệnh DDL tại thời điểm chạy một phần từ dữ liệu do người dùng nhập, để điền vào thân hàm kích hoạt. Cụ thể, chúng tôi viết mã cứng cho phác thảo của câu lệnh để tạo một vai trò đăng nhập mới và điền vào các tham số cụ thể dưới dạng các biến.

Dạng chung của lệnh này là

create role name [ [ with ] option [ ... ] ]

ở đâu tùy chọn có thể là bất kỳ thuộc tính nào trong số mười sáu thuộc tính cụ thể. Nói chung các giá trị mặc định là phù hợp nhưng chúng tôi sẽ trình bày rõ ràng về một số tùy chọn giới hạn và sử dụng biểu mẫu

create role name 
  with 
    login 
    inherit 
    nosuperuser 
    nocreatedb 
    nocreaterole 
    password ‘password’;

nơi chúng tôi sẽ chèn tên vai trò và mật khẩu do người dùng chỉ định tại thời điểm chạy.

Các câu lệnh được xây dựng động được gọi bằng lệnh thực thi:

execute command-string [ INTO [STRICT] target ] [ USING expression [, ... ] ];

mà cho các nhu cầu cụ thể của chúng tôi sẽ trông như thế nào

  execute 'create role '
    || new.login_name
    || ' with login inherit nosuperuser nocreatedb nocreaterole password '
    || quote_literal(new.login_pass);

trong đó hàm quote_literal trả về đối số chuỗi được trích dẫn phù hợp để sử dụng dưới dạng chuỗi ký tự để tuân thủ yêu cầu cú pháp rằng mật khẩu trên thực tế được trích dẫn ..

Khi đã tạo xong chuỗi lệnh, chúng tôi cung cấp chuỗi lệnh đó làm đối số cho lệnh thực thi pl / pgsql trong hàm kích hoạt.

Tổng hợp tất cả những thứ này lại với nhau trông giống như:

create or replace function person_iit()
  returns trigger
  set schema 'public'
  language plpgsql
  security definer
  as $$
  begin

  -- note this is for demonstration only. it is vulnerable to sql injection.

  execute 'create role '
    || new.login_name
    || ' with login inherit nosuperuser nocreatedb nocreaterole password '
    || quote_literal(new.login_pass);

  return new;
  end;
  $$;

Hãy thử!

Mọi thứ đã sẵn sàng, vì vậy hãy cùng quay! Đầu tiên, chúng tôi chuyển ủy quyền phiên sang người dùng ẩn danh và sau đó thực hiện chèn vào chế độ xem người:

set session authorization anonymous;
insert into person values ('alice', '1234');

Kết quả là người dùng mới alice đã được thêm vào bảng hệ thống:

\du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 alice     |                                                            | {}
 anonymous | No inheritance                                             | {}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Nó thậm chí còn hoạt động trực tiếp từ dòng lệnh của hệ điều hành bằng cách chuyển chuỗi lệnh SQL tới tiện ích máy khách psql để thêm bob người dùng:

$ psql sampledb anonymous <<< "insert into person values ('bob', '4321');"
INSERT 0 1

$ psql sampledb anonymous <<< "\du"
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 alice     |                                                            | {}
 anonymous | No inheritance                                             | {}
 bob       |                                                            | {}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Áp dụng một số áo giáp

Ví dụ ban đầu về chức năng kích hoạt dễ bị tấn công SQL injection, tức là một tác nhân đe dọa độc hại có thể tạo đầu vào dẫn đến truy cập trái phép. Ví dụ:khi được kết nối với vai trò người dùng ẩn danh, nỗ lực thực hiện điều gì đó ngoài phạm vi không thành công:

set session authorization anonymous;
drop user alice;
ERROR:  permission denied to drop role

Nhưng đầu vào độc hại sau đây tạo ra một vai trò siêu người dùng có tên là ‘eve’ (cũng như tài khoản mồi nhử có tên ‘cathy’):

insert into person 
  values ('eve with superuser login password ''666''; create role cathy', '777');
\du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 alice     |                                                            | {}
 anonymous | No inheritance                                             | {}
 cathy     |                                                            | {}
 eve       | Superuser                                                  | {}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Sau đó, vai trò siêu người dùng lén lút có thể được sử dụng để tàn phá cơ sở dữ liệu, ví dụ như xóa tài khoản người dùng (hoặc tệ hơn!):

\c - eve
drop user alice;
\du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 anonymous | No inheritance                                             | {}
 cathy     |                                                            | {}
 eve       | Superuser                                                  | {}
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Để giảm thiểu lỗ hổng này, chúng tôi phải thực hiện các bước để khử trùng đầu vào. Ví dụ:áp dụng hàm quote_ident, trả về một chuỗi được trích dẫn thích hợp để sử dụng làm mã định danh trong câu lệnh SQL với dấu ngoặc kép được thêm vào khi cần thiết, chẳng hạn như nếu chuỗi chứa các ký tự không phải mã định danh hoặc sẽ được viết hoa chữ thường và nhân đôi đúng cách được nhúng trích dẫn:

create or replace function person_iit()
  returns trigger
  set schema 'public'
  language plpgsql
  security definer
  as $$
  begin

  execute 'create role '
    || quote_ident(new.login_name)
    || ' with login inherit nosuperuser nocreatedb nocreaterole password '
    || quote_literal(new.login_pass);

  return new;
  end;
  $$;

Bây giờ, nếu cùng một khai thác SQL injection được cố gắng tạo một siêu người dùng khác có tên là ‘Frank’, nó không thành công và kết quả là một tên người dùng rất không chính thống:

set session authorization anonymous;
insert into person 
  values ('frank with superuser login password ''666''; create role dave', '777');
\du
                                 List of roles
    Role name          |                         Attributes                         | Member of 
-----------------------+------------------------------------------------------------+----------
 anonymous             | No inheritance                                             | {}
 eve                   | Superuser                                                  | {}
 frank with superuser  |                                                            |
  login password '666';|                                                            |
  create role dave     |                                                            |
 postgres              | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

Chúng tôi có thể áp dụng xác thực dữ liệu hợp lý hơn nữa trong hàm kích hoạt, chẳng hạn như chỉ yêu cầu tên người dùng gồm chữ và số và từ chối khoảng trắng và các ký tự khác:

create or replace function person_iit()
  returns trigger
  set schema 'public'
  language plpgsql
  security definer
  as $$
  begin

  -- Basic input sanitization

  if new.login_name is null then
    raise exception 'null login_name disallowed';
  elsif position(' ' in new.login_name) > 0 then
    raise exception 'login_name whitespace disallowed';
  elsif length(new.login_name) = 0 then
    raise exception 'login_name must be non-empty';
  elsif not (select new.login_name similar to '[A-Za-z]%') then
    raise exception 'login_name must begin with a letter.';
  end if;

  if new.login_pass is null then
    raise exception 'null login_pass disallowed';
  elsif position(' ' in new.login_pass) > 0 then
    raise exception 'login_pass whitespace disallowed';
  elsif length(new.login_pass) = 0 then
    raise exception 'login_pass must be non-empty';
  end if;

  -- Provision login credentials

  execute 'create role '
    || quote_ident(new.login_name)
    || ' with login inherit nosuperuser nocreatedb nocreaterole password '
    || quote_literal(new.login_pass);

  return new;
  end;
  $$;

và sau đó xác nhận rằng các kiểm tra vệ sinh khác nhau hoạt động:

set session authorization anonymous;
insert into person values (NULL, NULL);
ERROR:  null login_name disallowed
insert into person values ('gina', NULL);
ERROR:  null login_pass disallowed
insert into person values ('gina', '');
ERROR:  login_pass must be non-empty
insert into person values ('', '1234');
ERROR:  login_name must be non-empty
insert into person values ('gi na', '1234');
ERROR:  login_name whitespace disallowed
insert into person values ('1gina', '1234');
ERROR:  login_name must begin with a letter.

Hãy nâng tầm

Giả sử chúng ta muốn lưu trữ siêu dữ liệu hoặc dữ liệu ứng dụng bổ sung liên quan đến vai trò người dùng đã tạo, ví dụ:có thể là dấu thời gian và địa chỉ IP nguồn được liên kết với việc tạo vai trò. Chế độ xem tất nhiên không thể đáp ứng yêu cầu mới này vì không có bộ nhớ bên dưới, vì vậy cần phải có một bảng thực tế. Ngoài ra, hãy giả sử thêm rằng chúng tôi muốn hạn chế khả năng hiển thị của bảng đó khỏi những người dùng đăng nhập với vai trò đăng nhập ẩn danh. Chúng tôi có thể ẩn bảng trong một không gian tên riêng biệt (tức là một lược đồ PostgreSQL) mà người dùng ẩn danh vẫn không thể truy cập được. Hãy gọi không gian tên này là không gian tên “riêng tư” và tạo bảng trong không gian tên:

create schema private;

create table private.person (
  login_name   name not null primary key,
  inet_client_addr inet default inet_client_addr(),
  create_time timestamptz default now()  
);

Một lệnh chèn bổ sung đơn giản bên trong hàm kích hoạt ghi lại siêu dữ liệu được liên kết này:

create or replace function person_iit()
  returns trigger
  set schema 'public'
  language plpgsql
  security definer
  as $$
  begin

  -- Basic input sanitization
  if new.login_name is null then
    raise exception 'null login_name disallowed';
  elsif position(' ' in new.login_name) > 0 then
    raise exception 'login_name whitespace disallowed';
  elsif length(new.login_name) = 0 then
    raise exception 'login_name must be non-empty';
  elsif not (select new.login_name similar to '[A-Za-z]%') then
    raise exception 'login_name must begin with a letter.';
  end if;

  if new.login_pass is null then
    raise exception 'null login_pass disallowed';
  elsif length(new.login_pass) = 0 then
    raise exception 'login_pass must be non-empty';
  end if;

  -- Record associated metadata
  insert into private.person values (new.login_name);

  -- Provision login credentials

  execute 'create role '
    || quote_ident(new.login_name)
    || ' with login inherit nosuperuser nocreatedb nocreaterole password '
    || quote_literal(new.login_pass);

  return new;
  end;
  $$;

Và chúng tôi có thể cho nó một bài kiểm tra dễ dàng. Trước tiên, chúng tôi xác nhận rằng trong khi được kết nối dưới dạng vai trò ẩn danh, chỉ chế độ xem public. person mới hiển thị và không hiển thị bảng private. person:

set session authorization anonymous;

\d
         List of relations
 Schema |  Name  | Type |  Owner   
--------+--------+------+----------
 public | person | view | postgres
(1 row)
                   
select * from private.person;
ERROR:  permission denied for schema private

Và sau khi chèn vai trò mới:

insert into person values ('gina', '1234');

reset session authorization;

select * from private.person;
 login_name | inet_client_addr |          create_time          
------------+------------------+-------------------------------
 gina       | 192.168.2.106    | 2018-06-24 07:56:13.838679-07
(1 row)

bảng private. person hiển thị thu thập siêu dữ liệu cho địa chỉ IP và thời gian chèn hàng.

Kết luận

Trong bài viết này, chúng tôi đã trình bày một kỹ thuật để ủy quyền cung cấp thông tin xác thực vai trò PostgreSQL cho các vai trò không phải người dùng cấp cao. Mặc dù ví dụ đã ủy quyền đầy đủ chức năng ủy nhiệm cho người dùng ẩn danh, một cách tiếp cận tương tự có thể được sử dụng để ủy quyền một phần chức năng cho chỉ những người đáng tin cậy trong khi vẫn giữ được lợi ích của việc tải công việc này khỏi cơ sở dữ liệu giá trị cao hoặc nhân viên quản trị hệ thống. Chúng tôi cũng đã trình diễn một kỹ thuật truy cập dữ liệu phân lớp bằng cách sử dụng các lược đồ PostgreSQL, làm lộ hoặc ẩn các đối tượng cơ sở dữ liệu một cách có chọn lọc. Trong phần tiếp theo của loạt bài này, chúng tôi sẽ mở rộng về kỹ thuật truy cập dữ liệu phân lớp để đề xuất một thiết kế kiến ​​trúc cơ sở dữ liệu mới cho việc triển khai ứng dụng.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Khung thực thể PostgreSQL

  2. Lược đồ Django và postgresql

  3. Làm cách nào để giải quyết vấn đề xác thực Postgresql SCRAM?

  4. Bạn có thể tạo chỉ mục trong định nghĩa TẠO BẢNG không?

  5. Cần chuyển Oracle merge thành truy vấn sang PostgreSQL