Skip to content

Commit 9506d04

Browse files
jeltzlarskanis
authored andcommitted
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 4419c2e commit 9506d04

7 files changed

Lines changed: 132 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
@@ -965,6 +965,19 @@ pgconn_socket(VALUE self)
965965
return INT2NUM(sd);
966966
}
967967

968+
#ifdef _WIN32
969+
#define is_socket(fd) rb_w32_is_socket(fd)
970+
#else
971+
static int
972+
is_socket(int fd)
973+
{
974+
struct stat sbuf;
975+
976+
if (fstat(fd, &sbuf) < 0)
977+
rb_sys_fail("fstat(2)");
978+
return S_ISSOCK(sbuf.st_mode);
979+
}
980+
#endif
968981

969982
VALUE
970983
pg_wrap_socket_io(int sd, VALUE self, VALUE *p_socket_io, int *p_ruby_sd)
@@ -983,7 +996,7 @@ pg_wrap_socket_io(int sd, VALUE self, VALUE *p_socket_io, int *p_ruby_sd)
983996
*p_ruby_sd = ruby_sd = sd;
984997
#endif
985998

986-
cSocket = rb_const_get(rb_cObject, rb_intern("BasicSocket"));
999+
cSocket = rb_const_get(rb_cObject, rb_intern(is_socket(ruby_sd) ? "BasicSocket" : "IO"));
9871000
socket_io = rb_funcall( cSocket, rb_intern("for_fd"), 1, INT2NUM(ruby_sd));
9881001

9891002
/* Disable autoclose feature */

spec/helpers.rb

Lines changed: 18 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

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

0 commit comments

Comments
 (0)