File Coverage

blib/lib/MySQL/GrantParser.pm
Criterion Covered Total %
statement 92 129 71.3
branch 37 52 71.1
condition 2 9 22.2
subroutine 10 12 83.3
pod 2 6 33.3
total 143 208 68.7


line stmt bran cond sub pod time code
1             package MySQL::GrantParser;
2              
3 6     6   338676 use strict;
  6         56  
  6         175  
4 6     6   30 use warnings;
  6         12  
  6         137  
5 6     6   168 use 5.008_005;
  6         19  
6              
7             our $VERSION = '1.005';
8              
9 6     6   10338 use DBI;
  6         106719  
  6         341  
10 6     6   67 use Carp;
  6         12  
  6         10060  
11              
12             sub new {
13 1     1 1 677 my($class, %args) = @_;
14              
15 1         3 my $self = {
16             dbh => undef,
17             need_disconnect => 0,
18             };
19 1 50       4 if (exists $args{dbh}) {
20 1         3 $self->{dbh} = delete $args{dbh};
21             } else {
22 0 0 0     0 if (!$args{hostname} && !$args{socket}) {
23 0         0 Carp::croak("missing mandatory args: hostname or socket");
24             }
25              
26 0         0 my $dsn = "DBI:mysql:";
27 0         0 for my $p (
28             [qw(hostname hostname)],
29             [qw(port port)],
30             [qw(socket mysql_socket)],
31             ) {
32 0         0 my $arg_key = $p->[0];
33 0         0 my $param_key = $p->[1];
34 0 0       0 if ($args{$arg_key}) {
35 0         0 $dsn .= ";$param_key=$args{$arg_key}";
36             }
37             }
38              
39 0         0 $self->{need_disconnect} = 1;
40             $self->{dbh} = DBI->connect(
41             $dsn,
42             $args{user}||'',
43 0 0 0     0 $args{password}||'',
      0        
44             {
45             AutoCommit => 0,
46             },
47             ) or Carp::croak("$DBI::errstr ($DBI::err)");
48             }
49              
50 1 50       3 $self->{server_version} = exists $self->{dbh}->{mysql_serverversion} ? $self->{dbh}->{mysql_serverversion} : 0;
51              
52 1         3 return bless $self, $class;
53             }
54              
55             sub parse {
56 0     0 1 0 my $self = shift;
57 0         0 my %grants;
58              
59             # select all user
60 0         0 my $rset = $self->{dbh}->selectall_arrayref('SELECT user, host FROM mysql.user');
61              
62 0         0 for my $user_host (@$rset) {
63 0         0 my ($user, $host) = @{$user_host};
  0         0  
64 0         0 my $quoted_user_host = $self->quote_user($user, $host);
65 0         0 my $rset = $self->{dbh}->selectall_arrayref("SHOW GRANTS FOR ${quoted_user_host}");
66 0         0 my @stmts;
67 0         0 for my $rs (@$rset) {
68 0         0 push @stmts, @{$rs};
  0         0  
69             }
70 0 0       0 if ($self->{server_version} >= 50706) {
71             # As of MySQL 5.7.6, SHOW GRANTS output does not include IDENTIFIED BY PASSWORD clauses. Use the SHOW CREATE USER statement instead.
72             # https://dev.mysql.com/doc/refman/5.7/en/show-grants.html
73 0         0 my $rset = $self->{dbh}->selectall_arrayref("SHOW CREATE USER ${quoted_user_host}");
74 0         0 for my $rs (@$rset) {
75 0         0 push @stmts, @{$rs};
  0         0  
76             }
77             }
78              
79 0         0 %grants = (%grants, %{ parse_stmts(\@stmts) });
  0         0  
80             }
81              
82 0         0 return \%grants;
83             }
84              
85             sub parse_stmts {
86 14     14 0 17070 my $stmts = shift;
87 14         31 my @grants = ();
88 14         25 my $q = q{['`]};
89 14         26 my $Q = q{[^'`]};
90              
91 14         30 for my $stmt (@$stmts) {
92 31         135 my $parsed = {
93             with => '',
94             require => '',
95             identified => '',
96             privs => [],
97             object => '',
98             user => '',
99             host => '',
100             };
101              
102 31 100       409 if ($stmt =~ s/\AGRANT (.+?) ON (.+?) TO ${q}(${Q}+?)${q}\@${q}(${Q}+?)${q}\s*//) {
103 22         59 $parsed->{privs} = parse_privs($1);
104 22         72 $parsed->{object} = $2;
105 22         47 $parsed->{user} = $3;
106 22         61 $parsed->{host} = $4;
107             }
108 31 100       275 if ($stmt =~ s/\ACREATE USER ${q}(${Q}+?)${q}\@${q}(${Q}+?)${q}\s*//) {
109 9         34 $parsed->{user} = $1;
110 9         23 $parsed->{host} = $2;
111             }
112              
113 31 100       266 if ($stmt =~ s/\AIDENTIFIED BY PASSWORD ${q}(${Q}+?)${q}\s*//) {
114 3         11 $parsed->{identified} = "PASSWORD '$1'";
115             }
116 31 100       288 if ($stmt =~ s/\AIDENTIFIED WITH ${q}(${Q}+?)${q} AS ${q}(${Q}+?)${q}\s*//) {
117             # my $auth_plugin = $1; # eg: mysql_native_password
118 4         16 $parsed->{identified} = "PASSWORD '$2'";
119             }
120 31 100       209 if ($stmt =~ s/\AIDENTIFIED WITH ${q}(${Q}+?)${q}\s*//) {
121             # no AS
122             # my $auth_plugin = $1; # eg: mysql_native_password
123 5         15 $parsed->{identified} = '';
124             }
125              
126 31 100       109 if ($stmt =~ s/\AREQUIRE //) {
127 11 100       63 if ($stmt =~ s/\ANONE\s*//) {
    100          
128 7         15 $parsed->{require} = '';
129             } elsif ($stmt =~ s/\A(SSL|X509)\s*//) {
130 2         8 $parsed->{require} = $1;
131             } else {
132 2         11 my @tls_options = ();
133 2         177 while ($stmt =~ s/\A((?:CIPHER|ISSUER|SUBJECT) ${q}${Q}+?${q})\s*//g) {
134 4         39 push @tls_options, $1;
135             }
136 2         16 $parsed->{require} = join ' ', @tls_options;
137             }
138             }
139              
140 31 100       128 if ($stmt =~ s/\AWITH //) {
141 12         24 my @with = ();
142 12 100       44 if ($stmt =~ s/\AGRANT OPTION\s*//) {
143 8         26 push @with, 'GRANT OPTION';
144             }
145 12         52 while ($stmt =~ s/\A(MAX_\w+ \d+)\s*//g) {
146 11         29 push @with, $1;
147 11   100     66 $parsed->{object} ||= '*.*';
148             }
149 12         47 $parsed->{with} = join ' ', @with;
150             }
151              
152 31         82 push @grants, $parsed;
153             }
154              
155 14         38 return pack_grants(@grants);
156             }
157              
158             sub pack_grants {
159 14     14 0 39 my @grants = @_;
160 14         22 my $packed;
161              
162 14         34 for my $grant (@grants) {
163 31         63 my $user = delete $grant->{user};
164 31         54 my $host = delete $grant->{host};
165 31         64 my $user_host = join '@', $user, $host;
166 31         50 my $object = delete $grant->{object};
167 31         51 my $identified = delete $grant->{identified};
168 31         48 my $required = delete $grant->{require};
169              
170 31 100       90 unless (exists $packed->{$user_host}) {
171 14         78 $packed->{$user_host} = {
172             user => $user,
173             host => $host,
174             objects => {},
175             options => {
176             required => '',
177             identified => '',
178             },
179             };
180             }
181              
182 31 100       64 if ($object) {
183 26 100       91 if (exists $packed->{$user_host}{objects}{$object}) {
184 4 50       7 if (@{ $grant->{privs} }) {
  4         26  
185 0         0 push @{$packed->{$user_host}{objects}{$object}{privs}}, @{ $grant->{privs} };
  0         0  
  0         0  
186             }
187 4 50       11 if ($grant->{with}) {
188 4 100       16 if ($packed->{$user_host}{objects}{$object}{with}) {
189 1         4 $packed->{$user_host}{objects}{$object}{with} .= ' ' . $grant->{with};
190             } else {
191 3         12 $packed->{$user_host}{objects}{$object}{with} = $grant->{with};
192             }
193             }
194             } else {
195 22         47 $packed->{$user_host}{objects}{$object} = $grant;
196             }
197             }
198              
199 31 100       64 $packed->{$user_host}{options}{required} = $required if $required;
200              
201 31 100       73 if ($identified) {
202 7         16 $packed->{$user_host}{options}{identified} = $identified;
203             }
204             }
205              
206 14         73 return $packed;
207             }
208              
209             sub quote_user {
210 0     0 0 0 my $self = shift;
211 0         0 my($user, $host) = @_;
212 0         0 sprintf q{%s@%s}, $self->{dbh}->quote($user), $self->{dbh}->quote($host);
213             }
214              
215             sub parse_privs {
216 22     22 0 52 my $privs = shift;
217 22         44 $privs .= ',';
218              
219 22         34 my @priv_list = ();
220              
221 22         123 while ($privs =~ /\G([^,(]+(?:\([^)]+\))?)\s*,\s*/g) {
222 50         114 my $priv = $1;
223 50         81 $priv =~ s/`//g; # trim quote for MySQL 8.0
224 50         169 push @priv_list, $priv;
225             }
226              
227 22         67 return \@priv_list;
228             }
229              
230             sub DESTROY {
231 1     1   1168 my $self = shift;
232 1 50       128 if ($self->{need_disconnect}) {
233 0 0         $self->{dbh} && $self->{dbh}->disconnect;
234             }
235             }
236              
237             1;
238              
239             __END__