-
Notifications
You must be signed in to change notification settings - Fork 794
/
Copy pathmysql.rb
234 lines (207 loc) · 12.4 KB
/
mysql.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# frozen_string_literal: true
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'mysql'))
Puppet::Type.type(:mysql_grant).provide(:mysql, parent: Puppet::Provider::Mysql) do
desc 'Set grants for users in MySQL.'
commands mysql_raw: 'mysql'
def self.instances
instance_configs = {}
users.map do |user|
user_string = cmd_user(user)
query = "SHOW GRANTS FOR #{user_string};"
begin
grants = mysql_caller(query, 'regular')
rescue Puppet::ExecutionFailure => e
# Silently ignore users with no grants. Can happen e.g. if user is
# defined with fqdn and server is run with skip-name-resolve. Example:
# Default root user created by mysql_install_db on a host with fqdn
# of myhost.mydomain.my: [email protected], when MySQL is started
# with --skip-name-resolve.
next if e.inspect.include?('There is no such grant defined for user')
raise Puppet::Error, _('#mysql had an error -> %{inspect}') % { inspect: e.inspect }
end
# Once we have the list of grants generate entries for each.
grants.each_line do |grant|
# Match the munges we do in the type.
munged_grant = grant.delete("'").delete('`').delete('"')
# Matching: GRANT (SELECT, UPDATE) PRIVILEGES ON (*.*) TO ('root')@('127.0.0.1') (WITH GRANT OPTION)
next unless match = munged_grant.match(%r{^GRANT\s(.+)\sON\s(.+)\sTO\s(.*)@(.*?)(\s.*)?$}) # rubocop:disable Lint/AssignmentInCondition
privileges, table, user, host, rest = match.captures
table.gsub!('\\\\', '\\')
# split on ',' if it is not a non-'('-containing string followed by a
# closing parenthesis ')'-char - e.g. only split comma separated elements not in
# parentheses
stripped_privileges = privileges.strip.split(%r{\s*,\s*(?![^(]*\))}).map do |priv|
# split and sort the column_privileges in the parentheses and rejoin
if priv.include?('(')
type, col = priv.strip.split(%r{\s+|\b}, 2)
"#{type.upcase} (#{col.slice(1...-1).strip.split(%r{\s*,\s*}).sort.join(', ')})"
else
# Once we split privileges up on the , we need to make sure we
# shortern ALL PRIVILEGES to just all.
(priv == 'ALL PRIVILEGES') ? 'ALL' : priv.strip
end
end
# Same here, but to remove OPTION leaving just GRANT.
options = if %r{WITH\sGRANT\sOPTION}.match?(rest)
['GRANT']
else
['NONE']
end
# fix double backslash that MySQL prints, so resources match
table.gsub!('\\\\', '\\')
# We need to return an array of instances so capture these
name = "#{user}@#{host}/#{table}"
if instance_configs.key?(name)
instance_config = instance_configs[name]
stripped_privileges.concat instance_config[:privileges]
options.concat instance_config[:options]
end
sorted_privileges = stripped_privileges.uniq.sort
mysql_v8_privileges = ['ALTER', 'ALTER ROUTINE', 'CREATE', 'CREATE ROLE', 'CREATE ROUTINE', 'CREATE TABLESPACE', 'CREATE TEMPORARY TABLES', 'CREATE USER',
'CREATE VIEW', 'DELETE', 'DROP', 'DROP ROLE', 'EVENT', 'EXECUTE', 'FILE', 'INDEX', 'INSERT', 'LOCK TABLES', 'PROCESS', 'REFERENCES',
'RELOAD', 'REPLICATION CLIENT', 'REPLICATION SLAVE', 'SELECT', 'SHOW DATABASES', 'SHOW VIEW', 'SHUTDOWN', 'SUPER', 'TRIGGER',
'UPDATE']
# ndbcluster has its own set for ALL
mysqlcluster_v8_privileges = ['ALL', 'APPLICATION_PASSWORD_ADMIN', 'AUDIT_ABORT_EXEMPT', 'AUDIT_ADMIN', 'AUTHENTICATION_POLICY_ADMIN', 'BACKUP_ADMIN', 'BINLOG_ADMIN',
'BINLOG_ENCRYPTION_ADMIN', 'CLONE_ADMIN', 'CONNECTION_ADMIN', 'ENCRYPTION_KEY_ADMIN', 'FIREWALL_EXEMPT', 'FLUSH_OPTIMIZER_COSTS', 'FLUSH_STATUS', 'FLUSH_TABLES',
'FLUSH_USER_RESOURCES', 'GROUP_REPLICATION_ADMIN', 'GROUP_REPLICATION_STREAM', 'INNODB_REDO_LOG_ARCHIVE', 'INNODB_REDO_LOG_ENABLE', 'NDB_STORED_USER',
'PASSWORDLESS_USER_ADMIN', 'PERSIST_RO_VARIABLES_ADMIN', 'REPLICATION_APPLIER', 'REPLICATION_SLAVE_ADMIN', 'RESOURCE_GROUP_ADMIN', 'RESOURCE_GROUP_USER',
'ROLE_ADMIN', 'SENSITIVE_VARIABLES_OBSERVER', 'SERVICE_CONNECTION_ADMIN', 'SESSION_VARIABLES_ADMIN', 'SET_USER_ID', 'SHOW_ROUTINE', 'SYSTEM_USER',
'SYSTEM_VARIABLES_ADMIN', 'TABLE_ENCRYPTION_ADMIN', 'XA_RECOVER_ADMIN']
# The following two compare blocks are a workaround for issue #1474.
mysql_pre_v8_privileges_one = ['ALL', 'APPLICATION_PASSWORD_ADMIN', 'AUDIT_ABORT_EXEMPT', 'AUDIT_ADMIN', 'AUTHENTICATION_POLICY_ADMIN', 'BACKUP_ADMIN', 'BINLOG_ADMIN',
'BINLOG_ENCRYPTION_ADMIN', 'CLONE_ADMIN', 'CONNECTION_ADMIN', 'ENCRYPTION_KEY_ADMIN', 'FLUSH_OPTIMIZER_COSTS', 'FLUSH_STATUS',
'FLUSH_TABLES', 'FLUSH_USER_RESOURCES', 'GROUP_REPLICATION_ADMIN', 'GROUP_REPLICATION_STREAM', 'INNODB_REDO_LOG_ARCHIVE',
'INNODB_REDO_LOG_ENABLE', 'PASSWORDLESS_USER_ADMIN', 'PERSIST_RO_VARIABLES_ADMIN', 'REPLICATION_APPLIER', 'REPLICATION_SLAVE_ADMIN',
'RESOURCE_GROUP_ADMIN', 'RESOURCE_GROUP_USER', 'ROLE_ADMIN', 'SERVICE_CONNECTION_ADMIN', 'SESSION_VARIABLES_ADMIN', 'SET_USER_ID',
'SHOW_ROUTINE', 'SYSTEM_USER', 'SYSTEM_VARIABLES_ADMIN', 'TABLE_ENCRYPTION_ADMIN', 'XA_RECOVER_ADMIN']
mysql_pre_v8_privileges_two = ['ALL', 'APPLICATION_PASSWORD_ADMIN', 'AUDIT_ABORT_EXEMPT', 'AUDIT_ADMIN', 'AUTHENTICATION_POLICY_ADMIN', 'BACKUP_ADMIN', 'BINLOG_ADMIN',
'BINLOG_ENCRYPTION_ADMIN', 'CLONE_ADMIN', 'CONNECTION_ADMIN', 'ENCRYPTION_KEY_ADMIN', 'FLUSH_OPTIMIZER_COSTS', 'FLUSH_STATUS',
'FLUSH_TABLES', 'FLUSH_USER_RESOURCES', 'GROUP_REPLICATION_ADMIN', 'GROUP_REPLICATION_STREAM', 'INNODB_REDO_LOG_ARCHIVE',
'INNODB_REDO_LOG_ENABLE', 'PASSWORDLESS_USER_ADMIN', 'PERSIST_RO_VARIABLES_ADMIN', 'REPLICATION_APPLIER', 'REPLICATION_SLAVE_ADMIN',
'RESOURCE_GROUP_ADMIN', 'RESOURCE_GROUP_USER', 'ROLE_ADMIN', 'SENSITIVE_VARIABLES_OBSERVER', 'SERVICE_CONNECTION_ADMIN',
'SESSION_VARIABLES_ADMIN', 'SET_USER_ID', 'SHOW_ROUTINE', 'SYSTEM_USER', 'SYSTEM_VARIABLES_ADMIN', 'TABLE_ENCRYPTION_ADMIN', 'XA_RECOVER_ADMIN']
mysql_pre_v8_privileges_three = ['ALL', 'APPLICATION_PASSWORD_ADMIN', 'AUDIT_ABORT_EXEMPT', 'AUDIT_ADMIN', 'AUTHENTICATION_POLICY_ADMIN', 'BACKUP_ADMIN', 'BINLOG_ADMIN',
'BINLOG_ENCRYPTION_ADMIN', 'CLONE_ADMIN', 'CONNECTION_ADMIN', 'ENCRYPTION_KEY_ADMIN', 'FIREWALL_EXEMPT', 'FLUSH_OPTIMIZER_COSTS', 'FLUSH_STATUS',
'FLUSH_TABLES', 'FLUSH_USER_RESOURCES', 'GROUP_REPLICATION_ADMIN', 'GROUP_REPLICATION_STREAM', 'INNODB_REDO_LOG_ARCHIVE',
'INNODB_REDO_LOG_ENABLE', 'PASSWORDLESS_USER_ADMIN', 'PERSIST_RO_VARIABLES_ADMIN', 'REPLICATION_APPLIER', 'REPLICATION_SLAVE_ADMIN',
'RESOURCE_GROUP_ADMIN', 'RESOURCE_GROUP_USER', 'ROLE_ADMIN', 'SENSITIVE_VARIABLES_OBSERVER', 'SERVICE_CONNECTION_ADMIN',
'SESSION_VARIABLES_ADMIN', 'SET_USER_ID', 'SHOW_ROUTINE', 'SYSTEM_USER', 'SYSTEM_VARIABLES_ADMIN', 'TABLE_ENCRYPTION_ADMIN', 'TELEMETRY_LOG_ADMIN',
'XA_RECOVER_ADMIN']
# rubocop:disable Layout/LineLength
if (newer_than('mysql' => '8.0.0') && (sorted_privileges == mysql_v8_privileges || sorted_privileges == mysqlcluster_v8_privileges)) || sorted_privileges == mysql_pre_v8_privileges_one || sorted_privileges == mysql_pre_v8_privileges_two || sorted_privileges == mysql_pre_v8_privileges_three
sorted_privileges = ['ALL']
end
# rubocop:enable Layout/LineLength
instance_configs[name] = {
privileges: sorted_privileges,
table: table,
user: "#{user}@#{host}",
options: options.uniq
}
end
end
instances = []
instance_configs.map do |name, config|
instances << new(
name: name,
ensure: :present,
privileges: config[:privileges],
table: config[:table],
user: config[:user],
options: config[:options],
)
end
instances
end
def self.prefetch(resources)
users = instances
resources.each_key do |name|
if provider = users.find { |user| user.name == name } # rubocop:disable Lint/AssignmentInCondition
resources[name].provider = provider
end
end
end
def grant(user, table, privileges, options)
user_string = self.class.cmd_user(user)
priv_string = self.class.cmd_privs(privileges)
table_string = privileges.include?('PROXY') ? self.class.cmd_user(table) : self.class.cmd_table(table)
query = "GRANT #{priv_string}"
query += " ON #{table_string}"
query += " TO #{user_string}"
query += self.class.cmd_options(options) unless options.nil?
self.class.mysql_caller(query, 'system')
end
def create
grant(@resource[:user], @resource[:table], @resource[:privileges], @resource[:options])
@property_hash[:ensure] = :present
@property_hash[:table] = @resource[:table]
@property_hash[:user] = @resource[:user]
@property_hash[:options] = @resource[:options] if @resource[:options]
@property_hash[:privileges] = @resource[:privileges]
exists? ? (return true) : (return false)
end
def revoke(user, table, revoke_privileges = ['ALL'])
user_string = self.class.cmd_user(user)
table_string = revoke_privileges.include?('PROXY') ? self.class.cmd_user(table) : self.class.cmd_table(table)
priv_string = self.class.cmd_privs(revoke_privileges)
# revoke grant option needs to be a extra query, because
# "REVOKE ALL PRIVILEGES, GRANT OPTION [..]" is only valid mysql syntax
# if no ON clause is used.
# It hast to be executed before "REVOKE ALL [..]" since a GRANT has to
# exist to be executed successfully
if revoke_privileges.include?('ALL') && !revoke_privileges.include?('PROXY')
query = "REVOKE GRANT OPTION ON #{table_string} FROM #{user_string}"
self.class.mysql_caller(query, 'system')
end
query = "REVOKE #{priv_string} ON #{table_string} FROM #{user_string}"
self.class.mysql_caller(query, 'system')
end
def destroy
# if the user was dropped, it'll have been removed from the user hash
# as the grants are already removed by the DROP statement
if self.class.users.include? @property_hash[:user]
if @property_hash[:privileges].include?('PROXY')
revoke(@property_hash[:user], @property_hash[:table], @property_hash[:privileges])
else
revoke(@property_hash[:user], @property_hash[:table])
end
end
@property_hash.clear
exists? ? (return false) : (return true)
end
def exists?
@property_hash[:ensure] == :present || false
end
def flush
@property_hash.clear
self.class.mysql_caller('FLUSH PRIVILEGES', 'regular')
end
mk_resource_methods
def diff_privileges(privileges_old, privileges_new)
diff = { revoke: [], grant: [] }
if privileges_old.include? 'ALL'
diff[:revoke] = privileges_old
diff[:grant] = privileges_new
elsif privileges_new.include? 'ALL'
diff[:grant] = privileges_new
else
diff[:revoke] = privileges_old - privileges_new
diff[:grant] = privileges_new - privileges_old
end
diff
end
def privileges=(privileges)
diff = diff_privileges(@property_hash[:privileges], privileges)
revoke(@property_hash[:user], @property_hash[:table], diff[:revoke]) unless diff[:revoke].empty?
grant(@property_hash[:user], @property_hash[:table], diff[:grant], @property_hash[:options]) unless diff[:grant].empty?
@property_hash[:privileges] = privileges
self.privileges
end
def options=(options)
revoke(@property_hash[:user], @property_hash[:table])
grant(@property_hash[:user], @property_hash[:table], @property_hash[:privileges], options)
@property_hash[:options] = options
self.options
end
end