File Coverage

blib/lib/MySQL/GrantParser.pm
Criterion Covered Total %
statement 86 120 71.6
branch 33 46 71.7
condition 2 9 22.2
subroutine 10 12 83.3
pod 2 6 33.3
total 133 193 68.9


line stmt bran cond sub pod time code
1             package MySQL::GrantParser;
2              
3 5     5   272919 use strict;
  5         34  
  5         144  
4 5     5   26 use warnings;
  5         8  
  5         108  
5 5     5   114 use 5.008_005;
  5         16  
6              
7             our $VERSION = '1.004';
8              
9 5     5   8697 use DBI;
  5         90727  
  5         295  
10 5     5   48 use Carp;
  5         9  
  5         7441  
11              
12             sub new {
13 1     1 1 635 my($class, %args) = @_;
14              
15 1         3 my $self = {
16             dbh => undef,
17             need_disconnect => 0,
18             };
19 1 50       12 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 12     12 0 14443 my $stmts = shift;
87 12         26 my @grants = ();
88 12         22 my $q = q{['`]};
89 12         22 my $Q = q{[^'`]};
90              
91 12         30 for my $stmt (@$stmts) {
92 27         124 my $parsed = {
93             with => '',
94             require => '',
95             identified => '',
96             privs => [],
97             object => '',
98             user => '',
99             host => '',
100             };
101              
102 27 100       347 if ($stmt =~ s/\AGRANT (.+?) ON (.+?) TO ${q}(${Q}+?)${q}\@${q}(${Q}+?)${q}\s*//) {
103 20         54 $parsed->{privs} = parse_privs($1);
104 20         52 $parsed->{object} = $2;
105 20         44 $parsed->{user} = $3;
106 20         42 $parsed->{host} = $4;
107             }
108 27 100       209 if ($stmt =~ s/\ACREATE USER ${q}(${Q}+?)${q}\@${q}(${Q}+?)${q}\s*//) {
109 7         31 $parsed->{user} = $1;
110 7         16 $parsed->{host} = $2;
111             }
112              
113 27 100       214 if ($stmt =~ s/\AIDENTIFIED BY PASSWORD ${q}(${Q}+?)${q}\s*//) {
114 3         10 $parsed->{identified} = "PASSWORD '$1'";
115             }
116 27 100       233 if ($stmt =~ s/\AIDENTIFIED WITH ${q}(${Q}+?)${q} AS ${q}(${Q}+?)${q}\s*//) {
117             # my $auth_plugin = $1; # eg: mysql_native_password
118 4         34 $parsed->{identified} = "PASSWORD '$2'";
119             }
120 27 100       143 if ($stmt =~ s/\AIDENTIFIED WITH ${q}(${Q}+?)${q}\s*//) {
121             # no AS
122             # my $auth_plugin = $1; # eg: mysql_native_password
123 3         8 $parsed->{identified} = '';
124             }
125              
126 27 100       114 if ($stmt =~ s/\AREQUIRE //) {
127 9 100       54 if ($stmt =~ s/\ANONE\s*//) {
    100          
128 5         10 $parsed->{require} = '';
129             } elsif ($stmt =~ s/\A(SSL|X509)\s*//) {
130 2         18 $parsed->{require} = $1;
131             } else {
132 2         4 my @tls_options = ();
133 2         147 while ($stmt =~ s/\A((?:CIPHER|ISSUER|SUBJECT) ${q}${Q}+?${q})\s*//g) {
134 4         39 push @tls_options, $1;
135             }
136 2         13 $parsed->{require} = join ' ', @tls_options;
137             }
138             }
139              
140 27 100       97 if ($stmt =~ s/\AWITH //) {
141 9         20 my @with = ();
142 9 100       37 if ($stmt =~ s/\AGRANT OPTION\s*//) {
143 7         22 push @with, 'GRANT OPTION';
144             }
145 9         36 while ($stmt =~ s/\A(MAX_\w+ \d+)\s*//g) {
146 9         24 push @with, $1;
147 9   100     32 $parsed->{object} ||= '*.*';
148 9 100       14 $parsed->{privs} = ['USAGE'] unless @{ $parsed->{privs} };
  9         42  
149             }
150 9         33 $parsed->{with} = join ' ', @with;
151             }
152              
153 27         68 push @grants, $parsed;
154             }
155              
156 12         33 return pack_grants(@grants);
157             }
158              
159             sub pack_grants {
160 12     12 0 33 my @grants = @_;
161 12         43 my $packed;
162              
163 12         26 for my $grant (@grants) {
164 27         52 my $user = delete $grant->{user};
165 27         50 my $host = delete $grant->{host};
166 27         49 my $user_host = join '@', $user, $host;
167 27         42 my $object = delete $grant->{object};
168 27         41 my $identified = delete $grant->{identified};
169 27         41 my $required = delete $grant->{require};
170              
171 27 100       64 unless (exists $packed->{$user_host}) {
172 12         58 $packed->{$user_host} = {
173             user => $user,
174             host => $host,
175             objects => {},
176             options => {
177             required => '',
178             identified => '',
179             },
180             };
181             }
182 27 100       76 $packed->{$user_host}{objects}{$object} = $grant if $object;
183 27 100       53 $packed->{$user_host}{options}{required} = $required if $required;
184              
185 27 100       55 if ($identified) {
186 7         15 $packed->{$user_host}{options}{identified} = $identified;
187             }
188             }
189              
190 12         68 return $packed;
191             }
192              
193             sub quote_user {
194 0     0 0 0 my $self = shift;
195 0         0 my($user, $host) = @_;
196 0         0 sprintf q{%s@%s}, $self->{dbh}->quote($user), $self->{dbh}->quote($host);
197             }
198              
199             sub parse_privs {
200 20     20 0 45 my $privs = shift;
201 20         40 $privs .= ',';
202              
203 20         33 my @priv_list = ();
204              
205 20         112 while ($privs =~ /\G([^,(]+(?:\([^)]+\))?)\s*,\s*/g) {
206 42         88 my $priv = $1;
207 42         71 $priv =~ s/`//g; # trim quote for MySQL 8.0
208 42         143 push @priv_list, $priv;
209             }
210              
211 20         61 return \@priv_list;
212             }
213              
214             sub DESTROY {
215 1     1   1197 my $self = shift;
216 1 50       118 if ($self->{need_disconnect}) {
217 0 0         $self->{dbh} && $self->{dbh}->disconnect;
218             }
219             }
220              
221             1;
222              
223             __END__