omen 0.8.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50e0ccd4a4b0c3fa358e235be39a5e79b53fabbcb4e05c09424508cab6be99f9
4
- data.tar.gz: 4c4de5e474629c685455a599469c33caff9ae1e646dd331752313bf8c2e00020
3
+ metadata.gz: 9698c59cc037f0a9ccbe6b100af4a7137df0d26c8a54b3a8e5c37e9effe5e5e6
4
+ data.tar.gz: 792f03229094807c6ca995cc1271a199cc158db1f54ecfebee4a3e5db242712d
5
5
  SHA512:
6
- metadata.gz: 5158631649bc16dbeffbd630ba07303880657e30dd0c15ccba1ebde0a66317985caff827dfa423259d71091f74bf835634fa029050cd554917427784f9ef766e
7
- data.tar.gz: 91910028f557c76263c24b7c664aca12e6e89a8f5c009e605e4fd3563f1c39a8bbd35672dc24c3529c800a689b5c62a09a572e3e5e01f5072232ee8acf98db03
6
+ metadata.gz: 4395ce363d33978256ce75d72d6495c873af06299c140f0a61bf0fc637bda94bde5cbb129744447a01b6603ebbcaf4fb5567920d3a32c859ba86c097ef206354
7
+ data.tar.gz: 8f5c0384edd2be88dcaacb5a60cb9256c4f8c2139a5fcb351a7eae9fce3ff5619bf5403a919c9ebb6e61fe64103c94bcc80a58d9f656552568862f34ac6f8e9c
data/CHANGELOG.md CHANGED
@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
5
5
  For more information about changelogs, check [Keep a Changelog](http://keepachangelog.com) and
6
6
  [Vandamme](http://tech-angels.github.io/vandamme).
7
7
 
8
+ ## 0.10.1 - 2026-10-14
9
+
10
+ * [Change] The narrowed role is granted the connection's current schema rather than `public`,
11
+ so a host keeping its tables in a schema of its own -- sharing a database with another app,
12
+ each in a schema -- gets a role that reads its tables and no one else's. A host that never
13
+ set a search path reads `public` as before.
14
+
15
+ ## 0.9.0 - 2026-09-18
16
+
17
+ * [Fix] An answer landing touches the reading it belongs to, so a page watching one refreshes
18
+ on the answer rather than on whichever status change the run happens to make around it. A
19
+ question already touched its reading, which is why a question appeared and its answer did not.
20
+ * [Fix] Making an audience's role and letting members into it are two transactions, not one.
21
+ A managed database refuses to make a role at all, and a refusal takes its whole transaction
22
+ with it -- so the membership a role already there only needed was rolled back with the
23
+ refusal, and every statement run as it went on saying the app was misconfigured.
24
+ * [Change] An audience whose Postgres role could not be made says so once and narrows nothing,
25
+ rather than failing table by table and then reporting the audience narrowed. A managed
26
+ database refuses `CREATE ROLE` to the app's own user, and the refusal used to arrive as a
27
+ screenful of `role does not exist` followed by a success line.
28
+
8
29
  ## 0.8.0 - 2026-09-16
9
30
 
10
31
  * [Feature] Make how a Postgres role is made safely public, so a host builds the read-only role
data/README.md CHANGED
@@ -18,7 +18,7 @@ statement returned is ever sent back.
18
18
  In your `Gemfile`, pinned to the current minor while this is still below 1.0:
19
19
 
20
20
  ```ruby
21
- gem 'omen', '~> 0.6.0'
21
+ gem 'omen', '~> 0.10.0'
22
22
  ```
23
23
 
24
24
  Then four commands:
@@ -5,7 +5,10 @@ class Omen::Answer < Omen.config.record
5
5
  # A \uXXXX escape that outlived JSON.parse, because Claude escaped the backslash of its own.
6
6
  UNICODE_ESCAPE = /\\u([0-9a-fA-F]{4})/
7
7
 
8
- belongs_to :question
8
+ # Touched, so that a reading whose answer has just landed says so: the reading is what
9
+ # broadcasts, and the row it broadcasts for is this one. Without it a page waits on the
10
+ # status the run happens to change around the answer rather than on the answer itself.
11
+ belongs_to :question, touch: true
9
12
 
10
13
  # @return [String] the side of the conversation this was said on, in the words the API uses.
11
14
  def role = 'assistant'
data/lib/omen/bound.rb CHANGED
@@ -9,7 +9,7 @@ module Omen
9
9
  SELECT p.tablename, p.policyname, r.rolname FROM pg_policies p
10
10
  CROSS JOIN LATERAL unnest(p.roles) AS named(rolname)
11
11
  JOIN pg_roles r ON pg_has_role(r.oid, named.rolname::regrole::oid, 'USAGE')
12
- WHERE p.permissive = 'RESTRICTIVE' AND p.schemaname = 'public' AND NOT r.rolsuper
12
+ WHERE p.permissive = 'RESTRICTIVE' AND p.schemaname = current_schema() AND NOT r.rolsuper
13
13
  ORDER BY p.tablename, p.policyname, r.rolname
14
14
  SQL
15
15
 
data/lib/omen/grants.rb CHANGED
@@ -17,12 +17,13 @@ module Omen
17
17
  # @return [Array<String>] the statements to run, in order.
18
18
  def self.statements(connection, members)
19
19
  role = connection.quote_table_name Omen.config.narrow_role
20
+ read = schema connection
20
21
  [
21
22
  *made(connection, Omen.config.narrow_role),
22
23
  *members.map { |member| "GRANT #{role} TO #{connection.quote_table_name member}" },
23
- "GRANT USAGE ON SCHEMA public TO #{role}",
24
- "GRANT SELECT ON ALL TABLES IN SCHEMA public TO #{role}",
25
- "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO #{role}",
24
+ "GRANT USAGE ON SCHEMA #{read} TO #{role}",
25
+ "GRANT SELECT ON ALL TABLES IN SCHEMA #{read} TO #{role}",
26
+ "ALTER DEFAULT PRIVILEGES IN SCHEMA #{read} GRANT SELECT ON TABLES TO #{role}",
26
27
  *revoked(connection, role),
27
28
  *Omen::Renamed.statements,
28
29
  *Omen::TimeZone.statements(connection),
@@ -30,8 +31,10 @@ module Omen
30
31
  ]
31
32
  end
32
33
 
33
- # The same role, made for one audience rather than for every table: it is granted nothing
34
- # here, since a narrowing names the tables and the columns of each itself.
34
+ # Who may enter the role, and the schema it reads in. Kept apart from making the role: a
35
+ # managed database refuses to make one at all, and a refusal takes its whole transaction
36
+ # with it -- so membership granted in the same breath would be lost to a role that was
37
+ # already there and only needed letting into.
35
38
  # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
36
39
  # @param name [String] the role to make.
37
40
  # @param members [Array<String>] the roles that may enter it, none of them inheriting it.
@@ -39,12 +42,16 @@ module Omen
39
42
  def self.narrowed(connection, name, members)
40
43
  role = connection.quote_table_name name
41
44
  [
42
- *made(connection, name),
43
45
  *members.map { |member| apart role, connection.quote_table_name(member) },
44
- "GRANT USAGE ON SCHEMA public TO #{role}",
46
+ "GRANT USAGE ON SCHEMA #{schema connection} TO #{role}",
45
47
  ]
46
48
  end
47
49
 
50
+ # The schema a host keeps its tables in: the first of its search path, `public` by default.
51
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
52
+ # @return [String] its name, quoted.
53
+ def self.schema(connection) = connection.quote_table_name connection.current_schema
54
+
48
55
  # @param role [String] the role, quoted.
49
56
  # @param member [String] the role that may enter it, quoted.
50
57
  # @return [String] the statement that lets it in without handing it what the role holds.
data/lib/omen/inquirer.rb CHANGED
@@ -14,6 +14,13 @@ module Omen
14
14
  UNMADE = 'Could not make %{role}, so every reading will say this app is misconfigured. Ask ' \
15
15
  'for that role, NOLOGIN, granted SELECT on every table but %{tables}.'
16
16
 
17
+ # Said where an audience's own role could not be made. Its tables are then granted to
18
+ # nobody and no policy stands over them, so every reading of that audience says the app is
19
+ # misconfigured -- and every statement after the refusal fails for the same reason, which
20
+ # is one message worth saying and a screenful worth not.
21
+ UNNARROWED = 'Could not make %{role}, so nothing was narrowed to it and every reading of ' \
22
+ 'that audience will say this app is misconfigured. Ask for that role, NOLOGIN.'
23
+
17
24
  # Said where a role that already existed is one a reading should not be able to reach through.
18
25
  DANGEROUS = '%{role} holds %{held}. This gem cannot take that away without being a superuser ' \
19
26
  'itself, so ask for it to be taken away.'
@@ -58,10 +65,24 @@ module Omen
58
65
  # @param members [Array<String>] the roles that may enter a narrowed one.
59
66
  # @return [void]
60
67
  def self.narrow(connection, members)
61
- Omen::Reading.narrowings.each do |narrowing|
62
- narrowing.statements(connection, members).each { |group| Omen.attempted connection, *group }
63
- puts "Narrowed #{narrowing.role} to the rows #{narrowing.setting} names"
64
- end
68
+ Omen::Reading.narrowings.each { |narrowing| narrowed connection, narrowing, members }
69
+ end
70
+
71
+ # One audience: its role made first and asked for straight after, since everything else
72
+ # here names that role and a database that would not make it refuses the lot. Nothing is
73
+ # said to have been narrowed where nothing was.
74
+ # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
75
+ # @param narrowing [Omen::Narrowing] the audience to write.
76
+ # @param members [Array<String>] the roles that may enter its role.
77
+ # @return [void]
78
+ def self.narrowed(connection, narrowing, members)
79
+ making, *holding = narrowing.statements(connection, members)
80
+ Omen.attempted connection, *making
81
+ return warn UNNARROWED % { role: narrowing.role } unless
82
+ Attributes.exists? connection, narrowing.role
83
+
84
+ holding.each { |group| Omen.attempted connection, *group }
85
+ puts "Narrowed #{narrowing.role} to the rows #{narrowing.setting} names"
65
86
  end
66
87
 
67
88
  # Discovered rather than named: SET LOCAL ROLE needs the connecting role to be a member of
@@ -37,7 +37,8 @@ module Omen
37
37
  # @return [Array<Array<String>>] the statements, grouped: what arrives together is applied
38
38
  # together, so no table is left with row level security on and no policy under it.
39
39
  def statements(connection, members)
40
- [ Grants.narrowed(connection, @role, members),
40
+ [ Grants.made(connection, @role),
41
+ Grants.narrowed(connection, @role, members),
41
42
  *@whole.map { |table| granted connection, table },
42
43
  *@own.map { |table, own| narrowed connection, table, own }, ]
43
44
  end
data/lib/omen/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Omen
2
2
  # The version of this gem, as RubyGems knows it.
3
- VERSION = '0.8.0'
3
+ VERSION = '0.10.1'
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claudio Baccigalupo
@@ -146,7 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
146
146
  - !ruby/object:Gem::Version
147
147
  version: '0'
148
148
  requirements: []
149
- rubygems_version: 4.0.3
149
+ rubygems_version: 4.0.20
150
150
  specification_version: 4
151
151
  summary: Ask Claude about your own data
152
152
  test_files: []