Skip to content

Commit 87e9c71

Browse files
committed
WIP: Fix support for built-in OAuth hooks
Since the buitt-in OAuth hooks in libpq can return timerfd and not jsut a socket when you ask for the current file descriptor we are waiting on we need to make sure to use the right Ruby class to wrap the file descriptor, if it is not a valid socket we should use IO.
1 parent 01aa282 commit 87e9c71

7 files changed

Lines changed: 133 additions & 2 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
/lib/2.?/
1616
/lib/3.?/
1717
/pkg/
18+
/spec/oauth/*.bc
19+
/spec/oauth/*.o
20+
/spec/oauth/*.so
1821
/tmp/
1922
/tmp_test_*/
2023
/vendor/

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ group :test do
1515
gem "rake-compiler", "~> 1.0"
1616
gem "rake-compiler-dock", "~> 1.11.0" #, git: "https://github.com/rake-compiler/rake-compiler-dock"
1717
gem "rspec", "~> 3.5"
18+
gem "webrick", "~> 1.8"
1819
# "bigdecimal" is a gem on ruby-3.4+ and it's optional for ruby-pg.
1920
# Specs should succeed without it, but 4 examples are then excluded.
2021
# With bigdecimal commented out here, corresponding tests are omitted on ruby-3.4+ but are executed on ruby < 3.4.

ext/pg_connection.c

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,19 @@ pgconn_socket(VALUE self)
940940
return INT2NUM(sd);
941941
}
942942

943+
#ifdef _WIN32
944+
#define is_socket(fd) rb_w32_is_socket(fd)
945+
#else
946+
static int
947+
is_socket(int fd)
948+
{
949+
struct stat sbuf;
950+
951+
if (fstat(fd, &sbuf) < 0)
952+
rb_sys_fail("fstat(2)");
953+
return S_ISSOCK(sbuf.st_mode);
954+
}
955+
#endif
943956

944957
VALUE
945958
pg_wrap_socket_io(int sd, VALUE self, VALUE *p_socket_io, int *p_ruby_sd)
@@ -958,7 +971,7 @@ pg_wrap_socket_io(int sd, VALUE self, VALUE *p_socket_io, int *p_ruby_sd)
958971
*p_ruby_sd = ruby_sd = sd;
959972
#endif
960973

961-
cSocket = rb_const_get(rb_cObject, rb_intern("BasicSocket"));
974+
cSocket = rb_const_get(rb_cObject, rb_intern(is_socket(ruby_sd) ? "BasicSocket" : "IO"));
962975
socket_io = rb_funcall( cSocket, rb_intern("for_fd"), 1, INT2NUM(ruby_sd));
963976

964977
/* Disable autoclose feature */

spec/helpers.rb

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ class PostgresServer
194194
attr_reader :port
195195
attr_reader :conninfo
196196
attr_reader :unix_socket
197+
attr_reader :version
197198

198199
### Set up a PostgreSQL database instance for testing.
199200
def initialize(name, port: 23456, postgresql_conf: '')
@@ -205,6 +206,7 @@ def initialize(name, port: 23456, postgresql_conf: '')
205206
@pgdata = @test_dir + 'data'
206207
@logfile = @test_dir + 'setup.log'
207208
@pg_bindir = pg_bindir
209+
@version = pg_version
208210
@unix_socket = @test_dir.to_s
209211
@conninfo = "host=localhost port=#{@port} dbname=test sslrootcert=#{@pgdata + 'ruby-pg-ca-cert'} sslcert=#{@pgdata + 'ruby-pg-client-cert'} sslkey=#{@pgdata + 'ruby-pg-client-key'}"
210212

@@ -267,8 +269,13 @@ def setup_cluster(postgresql_conf)
267269
ssl_cert_file = 'ruby-pg-server-cert'
268270
ssl_key_file = 'ruby-pg-server-key'
269271
fsync = off
270-
#{postgresql_conf}
271272
EOT
273+
if @version >= 18
274+
fd.puts <<~EOT
275+
oauth_validator_libraries = '#{TEST_DIRECTORY}/spec/oauth/dummy_validator'
276+
EOT
277+
end
278+
fd.puts postgresql_conf
272279
end
273280

274281
# Enable MD5 authentication in hba config
@@ -278,6 +285,12 @@ def setup_cluster(postgresql_conf)
278285
# TYPE DATABASE USER ADDRESS METHOD
279286
host all testusermd5 ::1/128 md5
280287
EOT
288+
if @version >= 18
289+
fd.puts <<~EOT
290+
host all testuseroauth 127.0.0.1/32 oauth scope=test issuer="http://localhost:#{@port + 3}"
291+
host all testuseroauth ::1/32 oauth scope=test issuer="http://localhost:#{@port + 3}"
292+
EOT
293+
end
281294
fd.puts hba_content
282295
end
283296

@@ -340,6 +353,10 @@ def pg_bindir
340353
rescue
341354
nil
342355
end
356+
357+
def pg_version
358+
`#{pg_bin_path("pg_ctl")} --version`[/pg_ctl \(PostgreSQL\) (\d+)/, 1]&.to_i
359+
end
343360
end
344361

345362
class CertGenerator
@@ -682,6 +699,7 @@ def set_etc_hosts(hostaddr, hostname)
682699
config.filter_run_excluding( :postgresql_12 ) if PG.library_version < 120000
683700
config.filter_run_excluding( :postgresql_14 ) if PG.library_version < 140000
684701
config.filter_run_excluding( :postgresql_17 ) if PG.library_version < 170000
702+
config.filter_run_excluding( :postgresql_18 ) if PG.library_version < 180000
685703
config.filter_run_excluding( :unix_socket ) if RUBY_PLATFORM=~/mingw|mswin/i
686704
config.filter_run_excluding( :scheduler ) if RUBY_VERSION < "3.0" || (RUBY_PLATFORM =~ /mingw|mswin/ && RUBY_VERSION < "3.1") || !Fiber.respond_to?(:scheduler)
687705
config.filter_run_excluding( :scheduler_address_resolve ) if RUBY_VERSION < "3.1"

spec/oauth/Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
MODULES = dummy_validator
2+
PGFILEDESC = "dummy_validator - dummy OAuth validator"
3+
4+
OBJS = $(WIN32RES)
5+
6+
PG_CONFIG = pg_config
7+
PGXS := $(shell $(PG_CONFIG) --pgxs)
8+
include $(PGXS)

spec/oauth/dummy_validator.c

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#include "postgres.h"
2+
#include "fmgr.h"
3+
#include "libpq/oauth.h"
4+
5+
PG_MODULE_MAGIC;
6+
7+
static bool
8+
validate_token(const ValidatorModuleState *state,
9+
const char *token, const char *role,
10+
ValidatorModuleResult *res)
11+
{
12+
if (strcmp(token, "yes") == 0)
13+
{
14+
res->authorized = true;
15+
res->authn_id = pstrdup(role);
16+
}
17+
return true;
18+
}
19+
20+
static const OAuthValidatorCallbacks validator_callbacks = {
21+
PG_OAUTH_VALIDATOR_MAGIC,
22+
.validate_cb = validate_token
23+
};
24+
25+
const OAuthValidatorCallbacks *
26+
_PG_oauth_validator_module_init(void)
27+
{
28+
return &validator_callbacks;
29+
}

spec/pg/connection_spec.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2998,4 +2998,63 @@ def wait_check_socket(conn)
29982998
.to raise_error(TypeError)
29992999
end
30003000
end
3001+
3002+
describe "OAuth support", :postgresql_18 do
3003+
before :all do
3004+
skip "requires a PostgreSQL 18 cluster" unless $pg_server.version >= 18
3005+
3006+
system "make", "-s", "-C", (TEST_DIRECTORY + "spec/oauth").to_s
3007+
raise "Building OAuth validator library failed!" unless $?.success?
3008+
3009+
require 'webrick'
3010+
3011+
PG.connect(@conninfo) do |conn|
3012+
conn.exec("DROP USER IF EXISTS testuseroauth")
3013+
conn.exec("CREATE USER testuseroauth")
3014+
end
3015+
end
3016+
3017+
before :each do
3018+
@old_env, ENV["PGOAUTHDEBUG"] = ENV["PGOAUTHDEBUG"], "UNSAFE"
3019+
end
3020+
3021+
def start_fake_oauth(port)
3022+
server = WEBrick::HTTPServer.new(Port: port, Logger: WEBrick::Log.new(nil, WEBrick::BasicLog::WARN))
3023+
server.mount_proc("/.well-known/openid-configuration") do |req, res|
3024+
res["Content-Type"] = "application/json"
3025+
res.body = %!{"issuer":"http://localhost:#{port}","token_endpoint":"http://localhost:#{port}/token","device_authorization_endpoint":"http://localhost:#{@port + 3}/devauth"}!
3026+
end
3027+
server.mount_proc("/devauth") do |req, res|
3028+
res["Content-Type"] = "application/json"
3029+
res.body = %!{"device_code":"42","user_code":"666","verification_uri":"http://localhost:#{port}/verify","expires_in":60}!
3030+
end
3031+
server.mount_proc("/token") do |req, res|
3032+
res["Content-Type"] = "application/json"
3033+
res.body = %!{"access_token":"yes","token_type":""}!
3034+
end
3035+
Thread.new { server.start }
3036+
server
3037+
end
3038+
3039+
it "should work with no hook" do
3040+
oauth_server = start_fake_oauth(@port + 3)
3041+
3042+
begin
3043+
PG.connect("host=localhost port=#{@port} dbname=test user=testuseroauth oauth_issuer=http://localhost:#{@port + 3} oauth_client_id=foo") do |conn|
3044+
conn.exec("SELECT 1")
3045+
end
3046+
rescue PG::ConnectionBad => e
3047+
if e.message =~ /no OAuth flows are available/
3048+
skip "requires libpq-oauth to be installed"
3049+
end
3050+
raise
3051+
ensure
3052+
oauth_server.shutdown
3053+
end
3054+
end
3055+
3056+
after :each do
3057+
ENV["PGOAUTHDEBUG"] = @old_env
3058+
end
3059+
end
30013060
end

0 commit comments

Comments
 (0)