omen 0.3.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +4 -0
- data/lib/omen/attributes.rb +35 -0
- data/lib/omen/grants.rb +39 -0
- data/lib/omen/inquirer.rb +31 -41
- data/lib/omen/version.rb +1 -1
- data/lib/omen.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f96e34cbde627e0781a12488ba4308d704c6896a71fc44b031b7d412512c22eb
|
|
4
|
+
data.tar.gz: 408ad7c0b4a13f8ef3aef1b1c271e81ac116791ff627ffa2d056f5ecb6c45e34
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd53d5b7acb9fea6560bf0edbd5e1fb8f26200d99edaa52dfd9a81c9945dbd154a69514a7ba0028a025f36284bbd04be01e7aa57df61b7c01fa68ceb974638fd
|
|
7
|
+
data.tar.gz: a5b4226127e861f2600ebf62eb1d0c10ae2921886caf8e27b8041b411ac1b904c5e3406a0265f11e1f4c54a80ae9fa33994f396ec9adbc0df74c57b2304946df
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ For more information about changelogs, check [Keep a Changelog](http://keepachan
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## 0.3.1 - 2026-08-24
|
|
11
|
+
|
|
12
|
+
* [Fix] Stop asserting the role attributes only a superuser may set, since `NOSUPERUSER`,
|
|
13
|
+
`NOBYPASSRLS` and `NOREPLICATION` are refused outright by every managed database -- a role is
|
|
14
|
+
created without them anyway, so they are read back and warned about rather than set
|
|
15
|
+
* [Fix] Run each statement of the grant in a savepoint of its own, so one a database refuses no
|
|
16
|
+
longer discards the grants, the revocations and both functions behind it
|
|
17
|
+
|
|
10
18
|
## 0.3.0 - 2026-08-24
|
|
11
19
|
|
|
12
20
|
* [Feature] Create a `miles_between(lat1, lng1, lat2, lng2)` function and name it in the prompt,
|
data/README.md
CHANGED
|
@@ -147,6 +147,10 @@ usually does — the role is made by hand and the revocation with it:
|
|
|
147
147
|
REVOKE SELECT ON omen_readings, omen_questions, omen_answers FROM omen_inquirer;
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
A managed database also refuses `ALTER ROLE ... NOSUPERUSER`, since only a superuser may say it.
|
|
151
|
+
Omen skips that statement and carries on rather than stopping, then reads the role back and says
|
|
152
|
+
so if it holds `SUPERUSER`, `BYPASSRLS` or `REPLICATION` -- which a role it created never does.
|
|
153
|
+
|
|
150
154
|
A missed table there means Claude is shown the log of every question ever asked.
|
|
151
155
|
|
|
152
156
|
## License
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Omen
|
|
2
|
+
# What the role a statement runs as may and may not be. A role is created with every dangerous
|
|
3
|
+
# attribute already off, so this gem sets only what whoever created it may set, and reads the
|
|
4
|
+
# rest back rather than asserting them: saying `NOSUPERUSER` needs the SUPERUSER attribute, so
|
|
5
|
+
# a managed database refuses the very statement that would have made the role safe.
|
|
6
|
+
module Attributes
|
|
7
|
+
# Set on the role, because whoever may create a role may set these.
|
|
8
|
+
SETTABLE = 'NOLOGIN NOCREATEDB NOCREATEROLE'
|
|
9
|
+
|
|
10
|
+
# Read back instead, since saying no to one of these needs the attribute itself.
|
|
11
|
+
DANGEROUS = { rolsuper: 'SUPERUSER', rolbypassrls: 'BYPASSRLS',
|
|
12
|
+
rolreplication: 'REPLICATION', }
|
|
13
|
+
|
|
14
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
|
|
15
|
+
# @return [Boolean] whether the role is there at all to be granted anything.
|
|
16
|
+
def self.exists?(connection) = held(connection).present?
|
|
17
|
+
|
|
18
|
+
# Nil where there is no such role, so that an empty list and a missing role read apart.
|
|
19
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
|
|
20
|
+
# @return [Hash, nil] the row of what the role is, or nothing where the role is not.
|
|
21
|
+
def self.held(connection)
|
|
22
|
+
connection.select_one <<~SQL.squish
|
|
23
|
+
SELECT #{DANGEROUS.keys.join ', '} FROM pg_roles
|
|
24
|
+
WHERE rolname = #{connection.quote Omen.config.narrow_role}
|
|
25
|
+
SQL
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
|
|
29
|
+
# @return [Array<String>] what the role holds that no reading should be able to reach.
|
|
30
|
+
def self.dangerous(connection)
|
|
31
|
+
row = held(connection) || {}
|
|
32
|
+
DANGEROUS.filter_map { |column, name| name if row[column.to_s] }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/omen/grants.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Omen
|
|
2
|
+
# Everything said to a database to make the role a statement runs as: what it may be, what it
|
|
3
|
+
# may read, and what it may not. Kept apart from the task that runs them, which is about a
|
|
4
|
+
# database it has to find and a refusal it has to survive rather than about privileges.
|
|
5
|
+
module Grants
|
|
6
|
+
# DDL, which Active Record has no expression for, and not a query.
|
|
7
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
|
|
8
|
+
# @param members [Array<String>] the roles that may SET LOCAL ROLE to this one. The owner
|
|
9
|
+
# running the task is one of them, and matters in tests, where Rails swaps the reading
|
|
10
|
+
# pool for the writing one and the test connection is the owner.
|
|
11
|
+
# @return [Array<String>] the statements to run, in order.
|
|
12
|
+
def self.statements(connection, members)
|
|
13
|
+
role = connection.quote_table_name Omen.config.narrow_role
|
|
14
|
+
[
|
|
15
|
+
'DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = ' \
|
|
16
|
+
"#{connection.quote Omen.config.narrow_role}) THEN CREATE ROLE #{role} NOLOGIN; " \
|
|
17
|
+
'END IF; END $$',
|
|
18
|
+
"ALTER ROLE #{role} WITH #{Attributes::SETTABLE}",
|
|
19
|
+
"GRANT USAGE ON SCHEMA public TO #{role}",
|
|
20
|
+
"GRANT SELECT ON ALL TABLES IN SCHEMA public TO #{role}",
|
|
21
|
+
"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO #{role}",
|
|
22
|
+
*members.map { |member| "GRANT #{role} TO #{connection.quote_table_name member}" },
|
|
23
|
+
*revoked(connection, role),
|
|
24
|
+
*Omen::Eastern.statements(connection),
|
|
25
|
+
*Omen::Distance.statements(connection),
|
|
26
|
+
]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Intersected, so a bare db:create with no table yet to revoke on is not a failure.
|
|
30
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
|
|
31
|
+
# @param role [String] the role to hide this feature's own tables from.
|
|
32
|
+
# @return [Array<String>] one REVOKE per table there is.
|
|
33
|
+
def self.revoked(connection, role)
|
|
34
|
+
(connection.tables & Omen.tables).map do |table|
|
|
35
|
+
"REVOKE SELECT ON #{connection.quote_table_name table} FROM #{role}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/omen/inquirer.rb
CHANGED
|
@@ -3,9 +3,6 @@ module Omen
|
|
|
3
3
|
# It needs no database.yml entry of its own: NOLOGIN, it is a privilege container reached
|
|
4
4
|
# with SET LOCAL ROLE and never connected as.
|
|
5
5
|
module Inquirer
|
|
6
|
-
# What this role may never be: a superuser bypasses GRANT outright.
|
|
7
|
-
ATTRIBUTES = 'NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS NOREPLICATION'
|
|
8
|
-
|
|
9
6
|
# Whom to ask, since the role a host reads through is one this gem has no name for.
|
|
10
7
|
WHOEVER = 'SELECT current_user'
|
|
11
8
|
|
|
@@ -13,6 +10,18 @@ module Omen
|
|
|
13
10
|
UNGRANTED = 'No %{role} connection is configured, so nothing was granted to whatever ' \
|
|
14
11
|
'reads through it. Run this again once config/database.yml names one.'
|
|
15
12
|
|
|
13
|
+
# Said where the role could not be made: a managed database never grants CREATEROLE.
|
|
14
|
+
UNMADE = 'Could not make %{role}, so every reading will say this app is misconfigured. Ask ' \
|
|
15
|
+
'for that role, NOLOGIN, granted SELECT on every table but %{tables}.'
|
|
16
|
+
|
|
17
|
+
# Said per statement, since the grants, the revocations and the functions need nothing from
|
|
18
|
+
# one another and one refusal should not discard the rest.
|
|
19
|
+
REFUSED = 'Skipped, refused by the database: %{statement} (%{error})'
|
|
20
|
+
|
|
21
|
+
# Said where a role that already existed is one a reading should not be able to reach through.
|
|
22
|
+
DANGEROUS = '%{role} holds %{held}. This gem cannot take that away without being a superuser ' \
|
|
23
|
+
'itself, so ask for it to be taken away.'
|
|
24
|
+
|
|
16
25
|
# Creates the role and the function, on every database this environment prepares.
|
|
17
26
|
# @return [void]
|
|
18
27
|
def self.grant
|
|
@@ -37,12 +46,26 @@ module Omen
|
|
|
37
46
|
read_by = reader
|
|
38
47
|
warn UNGRANTED % { role: Omen.config.reading_role } unless read_by
|
|
39
48
|
members = [ read_by, connection.select_value(WHOEVER) ].compact
|
|
40
|
-
statements(connection, members).each { |statement| connection
|
|
41
|
-
|
|
49
|
+
Grants.statements(connection, members).each { |statement| attempted connection, statement }
|
|
50
|
+
role = Omen.config.narrow_role
|
|
51
|
+
return warn UNMADE % { role: role, tables: Omen.tables.to_sentence } unless
|
|
52
|
+
Attributes.exists? connection
|
|
53
|
+
|
|
54
|
+
held = Attributes.dangerous connection
|
|
55
|
+
warn DANGEROUS % { role: role, held: held.to_sentence } if held.any?
|
|
56
|
+
puts "Granted SELECT on #{connection.current_database} to #{role}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# One statement at a time, so a database that refuses one still runs the others -- and each
|
|
60
|
+
# inside a savepoint of its own, since a refusal inside a transaction refuses everything
|
|
61
|
+
# after it too, and this task is as likely to be run from a console as from a deploy.
|
|
62
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
|
|
63
|
+
# @param statement [String] one of the statements Omen::Grants builds.
|
|
64
|
+
# @return [void]
|
|
65
|
+
def self.attempted(connection, statement)
|
|
66
|
+
connection.transaction(requires_new: true) { connection.execute statement }
|
|
42
67
|
rescue ActiveRecord::StatementInvalid => error
|
|
43
|
-
warn
|
|
44
|
-
'misconfigured. Ask for that role, NOLOGIN, granted SELECT on every table but ' \
|
|
45
|
-
"#{Omen.tables.to_sentence}: #{error.message}"
|
|
68
|
+
warn REFUSED % { statement: statement.squish, error: error.message.lines.first.strip }
|
|
46
69
|
end
|
|
47
70
|
|
|
48
71
|
# Discovered rather than named: SET LOCAL ROLE needs the connecting role to be a member of
|
|
@@ -55,38 +78,5 @@ module Omen
|
|
|
55
78
|
rescue ActiveRecord::ConnectionNotDefined, ActiveRecord::ConnectionNotEstablished
|
|
56
79
|
nil
|
|
57
80
|
end
|
|
58
|
-
|
|
59
|
-
# DDL, which Active Record has no expression for, and not a query.
|
|
60
|
-
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
|
|
61
|
-
# @param members [Array<String>] the roles that may SET LOCAL ROLE to this one. The owner
|
|
62
|
-
# running the task is one of them, and matters in tests, where Rails swaps the reading
|
|
63
|
-
# pool for the writing one and the test connection is the owner.
|
|
64
|
-
# @return [Array<String>] the statements to run, in order.
|
|
65
|
-
def self.statements(connection, members)
|
|
66
|
-
role = connection.quote_table_name Omen.config.narrow_role
|
|
67
|
-
[
|
|
68
|
-
'DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = ' \
|
|
69
|
-
"#{connection.quote Omen.config.narrow_role}) THEN CREATE ROLE #{role} NOLOGIN; " \
|
|
70
|
-
'END IF; END $$',
|
|
71
|
-
"ALTER ROLE #{role} WITH NOLOGIN #{ATTRIBUTES}",
|
|
72
|
-
"GRANT USAGE ON SCHEMA public TO #{role}",
|
|
73
|
-
"GRANT SELECT ON ALL TABLES IN SCHEMA public TO #{role}",
|
|
74
|
-
"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO #{role}",
|
|
75
|
-
*members.map { |member| "GRANT #{role} TO #{connection.quote_table_name member}" },
|
|
76
|
-
*revoked(connection, role),
|
|
77
|
-
*Eastern.statements(connection),
|
|
78
|
-
*Distance.statements(connection),
|
|
79
|
-
]
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Intersected, so a bare db:create with no table yet to revoke on is not a failure.
|
|
83
|
-
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] a writing one.
|
|
84
|
-
# @param role [String] the role to hide this feature's own tables from.
|
|
85
|
-
# @return [Array<String>] one REVOKE per table there is.
|
|
86
|
-
def self.revoked(connection, role)
|
|
87
|
-
(connection.tables & Omen.tables).map do |table|
|
|
88
|
-
"REVOKE SELECT ON #{connection.quote_table_name table} FROM #{role}"
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
81
|
end
|
|
92
82
|
end
|
data/lib/omen/version.rb
CHANGED
data/lib/omen.rb
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
require 'active_job/performs'
|
|
2
2
|
require 'anthropic'
|
|
3
3
|
|
|
4
|
+
require 'omen/attributes'
|
|
4
5
|
require 'omen/config'
|
|
5
6
|
require 'omen/distance'
|
|
6
7
|
require 'omen/eastern'
|
|
8
|
+
require 'omen/grants'
|
|
7
9
|
require 'omen/inquirer'
|
|
8
10
|
require 'omen/requirements'
|
|
9
11
|
require 'omen/version'
|
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.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Claudio Baccigalupo
|
|
@@ -97,10 +97,12 @@ files:
|
|
|
97
97
|
- lib/generators/omen/install/install_generator.rb
|
|
98
98
|
- lib/generators/omen/install/templates/omen.rb
|
|
99
99
|
- lib/omen.rb
|
|
100
|
+
- lib/omen/attributes.rb
|
|
100
101
|
- lib/omen/config.rb
|
|
101
102
|
- lib/omen/distance.rb
|
|
102
103
|
- lib/omen/eastern.rb
|
|
103
104
|
- lib/omen/engine.rb
|
|
105
|
+
- lib/omen/grants.rb
|
|
104
106
|
- lib/omen/inquirer.rb
|
|
105
107
|
- lib/omen/requirements.rb
|
|
106
108
|
- lib/omen/stubs.rb
|