safer_redis 1.0.0

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.
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module SaferRedis
6
+ class CommandDoc
7
+ CATEGORY_SLOW = "@slow"
8
+ CATEGORY_DANGEROUS = "@dangerous"
9
+
10
+ # JSON-parsed Redis command data from the embedded copy of
11
+ # https://github.com/redis/redis-doc/blob/master/commands.json
12
+ def self.commands_data
13
+ @commands_data ||= begin
14
+ base_dir = File.absolute_path(File.join(__dir__, "..", ".."))
15
+ path = File.join(base_dir, "data", "redis-doc", "commands.json")
16
+
17
+ JSON.parse(File.read(path))
18
+ end
19
+ end
20
+
21
+ # The `redis` gem represents commands internally as an array with the command name
22
+ # as a lower-case symbol as the first item, e.g. [:del, "foo", "bar"]
23
+ def self.from_command_array(a)
24
+ new(a.first.to_s.upcase)
25
+ end
26
+
27
+ def initialize(name)
28
+ @name = name
29
+ end
30
+
31
+ attr_reader :name
32
+
33
+ def url
34
+ slug = name.downcase.gsub(" ", "-")
35
+
36
+ "https://redis.io/commands/#{slug}/"
37
+ end
38
+
39
+ def slow?
40
+ acl_categories.include?(CATEGORY_SLOW)
41
+ end
42
+
43
+ def dangerous?
44
+ acl_categories.include?(CATEGORY_DANGEROUS)
45
+ end
46
+
47
+ def acl_categories
48
+ command.fetch("acl_categories", [])
49
+ end
50
+
51
+ def complexity
52
+ command.fetch("complexity", nil)
53
+ end
54
+
55
+ private
56
+
57
+ def command
58
+ @command ||= (self.class.commands_data[name] || {})
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SaferRedis
4
+ class Danger < Error
5
+ def initialize(doc)
6
+ message = <<~MESSAGE
7
+ The #{doc.name} Redis command might be dangerous.
8
+
9
+ #{doc.url}
10
+
11
+ ACL categories: #{doc.acl_categories.join(" ")}
12
+
13
+ Complexity: #{doc.complexity}
14
+
15
+ If you're sure this is okay, you can try again within `SaferRedis.really { ... }`
16
+ MESSAGE
17
+
18
+ super(message)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SaferRedis
4
+ module Interceptor
5
+ def send_command(command, &block)
6
+ if SaferRedis.active?
7
+ SaferRedis.assess!(SaferRedis::CommandDoc.from_command_array(command))
8
+ end
9
+
10
+ super
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SaferRedis
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+ Zeitwerk::Loader.for_gem.setup
5
+
6
+ module SaferRedis
7
+ class Error < StandardError; end
8
+
9
+ def self.activate!(klass: Redis)
10
+ klass.prepend(SaferRedis::Interceptor)
11
+ @active = true
12
+ end
13
+
14
+ def self.deactivate!
15
+ @active = false
16
+ end
17
+
18
+ def self.active?
19
+ defined?(@active) ? @active : false
20
+ end
21
+
22
+ def self.really
23
+ was = active?
24
+ @active = false
25
+ yield
26
+ ensure
27
+ @active = was
28
+ end
29
+
30
+ def self.assess!(doc)
31
+ if doc.dangerous? || doc.slow?
32
+ raise SaferRedis::Danger.new(doc)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/safer_redis/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "safer_redis"
7
+ spec.version = SaferRedis::VERSION
8
+ spec.authors = ["Paul Annesley"]
9
+ spec.email = ["paul@annesley.cc"]
10
+
11
+ spec.summary = "Catch unsafe Redis commands in production"
12
+ spec.description = "SaferRedis warns you before letting through commands that could impact production availability by being marked `@slow` or `@dangerous` in the Redis documentation"
13
+ spec.homepage = "https://github.com/buildkite/safer_redis"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 2.6.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/buildkite/safer_redis"
19
+ spec.metadata["changelog_uri"] = "https://github.com/buildkite/safer_redis/blob/main/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(__dir__) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
26
+ end
27
+ end
28
+ spec.require_paths = ["lib"]
29
+
30
+ # The only strategy implemented so far is intercepting the private Redis#send_command method,
31
+ # which was introduced in v4.6.0 via https://github.com/redis/redis-rb/pull/1058 and remains
32
+ # in the latest release (v5.0.5) at time of writing.
33
+ spec.add_dependency "redis", ">= 4.6.0"
34
+ spec.add_dependency "zeitwerk"
35
+ end
@@ -0,0 +1,4 @@
1
+ module SaferRedis
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: safer_redis
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul Annesley
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-12-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 4.6.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 4.6.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: zeitwerk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: SaferRedis warns you before letting through commands that could impact
42
+ production availability by being marked `@slow` or `@dangerous` in the Redis documentation
43
+ email:
44
+ - paul@annesley.cc
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".rspec"
50
+ - CHANGELOG.md
51
+ - CODE_OF_CONDUCT.md
52
+ - Gemfile
53
+ - Gemfile.lock
54
+ - LICENSE.txt
55
+ - README.md
56
+ - Rakefile
57
+ - data/redis-doc/commands.json
58
+ - lib/safer_redis.rb
59
+ - lib/safer_redis/command_doc.rb
60
+ - lib/safer_redis/danger.rb
61
+ - lib/safer_redis/interceptor.rb
62
+ - lib/safer_redis/version.rb
63
+ - safer_redis.gemspec
64
+ - sig/safer_redis.rbs
65
+ homepage: https://github.com/buildkite/safer_redis
66
+ licenses:
67
+ - MIT
68
+ metadata:
69
+ homepage_uri: https://github.com/buildkite/safer_redis
70
+ source_code_uri: https://github.com/buildkite/safer_redis
71
+ changelog_uri: https://github.com/buildkite/safer_redis/blob/main/CHANGELOG.md
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: 2.6.0
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubygems_version: 3.1.6
88
+ signing_key:
89
+ specification_version: 4
90
+ summary: Catch unsafe Redis commands in production
91
+ test_files: []