File Coverage

blib/lib/Data/Validate/OpenAPI.pm
Criterion Covered Total %
statement 58 80 72.5
branch 20 48 41.6
condition 9 21 42.8
subroutine 13 14 92.8
pod 2 2 100.0
total 102 165 61.8


line stmt bran cond sub pod time code
1             package Data::Validate::OpenAPI;
2              
3 2     2   917 use strict;
  2         10  
  2         52  
4 2     2   10 use warnings;
  2         4  
  2         74  
5              
6             # ABSTRACT: Validate and untaint input parameters via OpenAPI schema
7             our $VERSION = '0.3.0'; # VERSION
8              
9 2     2   856 use OpenAPI::Render;
  2         2035  
  2         64  
10 2     2   817 use parent OpenAPI::Render::;
  2         587  
  2         13  
11              
12 2     2   1038 use Data::Validate qw( is_integer );
  2         127378  
  2         163  
13 2     2   935 use Data::Validate::Email qw( is_email );
  2         80399  
  2         144  
14 2     2   1246 use Data::Validate::IP qw( is_ipv4 is_ipv6 );
  2         69002  
  2         173  
15 2     2   1172 use Data::Validate::URI qw( is_uri );
  2         1832  
  2         119  
16 2     2   889 use DateTime::Format::RFC3339;
  2         1027400  
  2         83  
17 2     2   26 use Scalar::Util qw( blessed );
  2         5  
  2         1907  
18              
19             =head1 SYNOPSIS
20              
21             use CGI;
22             use Data::Validate::OpenAPI;
23              
24             my $validator = Data::Validate::OpenAPI->new( $parsed_openapi_json );
25             my $params = $validator->validate( '/', 'post', CGI->new );
26              
27             =head1 DESCRIPTION
28              
29             C<Data::Validate::OpenAPI> validates and untaints CGI parameters using a supplied OpenAPI schema.
30             It applies format-specific validation and untainting using appropriate L<Data::Validate> subclasses, including email, IP, URI and other.
31             Also it checks values against enumerators and patterns, if provided.
32             At this point values without supported formats, enumerators or patterns are returned as they are, tainted.
33             This behavior may change in the future.
34              
35             C<Data::Validate::OpenAPI> does not validate OpenAPI schemas.
36             To do so, refer to L<JSON::Validator>.
37              
38             =head1 METHODS
39              
40             =head2 C<new( $api )>
41              
42             Takes a parsed OpenAPI schema as returned by L<JSON> module's C<decode_json()>.
43             Returns validator ready to validate CGI parameters.
44              
45             =head2 C<validate( $path, $method, $cgi )>
46              
47             Takes a call path, HTTP method and a CGI object.
48             Returns a hash of validated pairs of CGI parameter keys and their values.
49             At this point values failing to validate are not reported.
50             Keys for parameters having no valid values are omitted from the returned hash.
51              
52             The interface for this method is bound to change, but backwards compatibility will be preserved on best effort basis.
53              
54             =cut
55              
56             sub validate
57             {
58 7     7 1 29463 my( $self, $path, $method, $input ) = @_;
59              
60             # FIXME: More specific parameters override less specific ones.
61             # FIXME: Request body parameters should be taken from CGI object
62             # using their own specific methods.
63             # TODO: In future, parameters other than 'query' can be returned too.
64 7         23 my $api = $self->{api};
65             my @parameters =
66 7         31 grep { $_->{in} eq 'query' }
67             exists $api->{paths}{$path}{parameters}
68 0         0 ? @{$api->{paths}{$path}{parameters}} : (),
69             exists $api->{paths}{$path}{$method}{parameters}
70 7         38 ? @{$api->{paths}{$path}{$method}{parameters}} : (),
71             exists $api->{paths}{$path}{$method}{requestBody}
72 7 50       45 ? OpenAPI::Render::RequestBody2Parameters( $api->{paths}{$path}{$method}{requestBody} ) : ();
    50          
    50          
73              
74 7         15 my $par = {};
75 7         13 my $par_hash = $input;
76              
77 7 50       28 if( blessed $par_hash ) {
78 0         0 $par_hash = { $par_hash->Vars }; # object is assumed to be CGI
79             }
80              
81 7         17 for my $description (@parameters) {
82 7         14 my $name = $description->{name};
83 7 50       22 my $schema = $description->{schema} if $description->{schema};
84 7 100       20 if( !exists $par_hash->{$name} ) {
85 1 50 33     9 if( $schema && exists $schema->{default} ) {
86 0         0 $par->{$name} = $schema->{default};
87             }
88 1         3 next;
89             }
90              
91 6 50 33     57 if( $schema && $schema->{type} eq 'array' ) {
92 0         0 my( @good_values, @bad_values );
93 0 0       0 for (ref $par_hash->{$name} eq 'ARRAY' ? @{$par_hash->{$name}} : split "\0", $par_hash->{$name}) {
  0         0  
94 0         0 my $value = _validate_value( $_, $schema );
95 0 0       0 push @good_values, $value if defined $value;
96 0 0       0 push @bad_values, $value unless defined $value;
97             }
98 0 0       0 $par->{$name} = \@good_values if @good_values;
99 0 0       0 $self->_report( $name, @bad_values ) if @bad_values;
100             } else {
101 6         20 my $value = _validate_value( $par_hash->{$name}, $schema );
102 6 100       2406 $par->{$name} = $value if defined $value;
103 6 100       25 $self->_report( $name, $value ) unless defined $value;
104             }
105             }
106              
107 7         24 return $par;
108             }
109              
110             =head1 VALIDATION ERROR REPORTING
111              
112             By default validation errors are silent, but there are two ways to handle validation errors: by setting validator-specific subroutine or by setting module variable:
113              
114             my $reporter_sub = sub { warn "value for '$_[0]' is incorrect" };
115              
116             # Set a reporter for this particular validator instance:
117             $validator->reporter( $reporter_sub );
118              
119             # Set a reporter for all instances of this class:
120             $Data::Validate::OpenAPI::reporter = $reporter_sub;
121              
122             If any of them is set, reporter subroutine is called with the following parameters:
123              
124             $reporter_sub->( $parameter_name, @bad_values );
125              
126             Validator-specific reporter takes precedence.
127             At this point the module does not indicate which particular check failed during the validation.
128              
129             =cut
130              
131             # Global variable for reporter subroutine
132             our $reporter;
133              
134             =head2 C<reporter( $reporter_sub )> method
135              
136             Set reporter subroutine to be called for each parameter failing the validation:
137              
138             $reporter_sub->( $parameter_name, @bad_values );
139              
140             =cut
141              
142             sub reporter
143             {
144 0     0 1 0 my( $self, $reporter_sub ) = @_;
145 0         0 $self->{reporter} = $reporter_sub;
146             }
147              
148             sub _report
149             {
150 2     2   5 my( $self, $name, @values ) = @_;
151              
152 2 50       11 if( $self->{reporter} ) {
    50          
153 0         0 $self->{reporter}->( $name, @values );
154             } elsif( $reporter ) {
155 0         0 $reporter->( $name, @values );
156             }
157             }
158              
159             my %format_methods = (
160             'date-time' => sub { my $parser = DateTime::Format::RFC3339->new;
161             return $parser->format_datetime( $parser->parse_datetime( $_[0] ) ) },
162             email => \&is_email,
163             integer => \&is_integer,
164             ipv4 => \&is_ipv4,
165             ipv6 => \&is_ipv6,
166             uri => \&is_uri,
167              
168             # Regex is taken from Data::Validate::UUID.
169             # The module itself is not used as it does not untaint the value.
170             uuid => sub { return $1 if $_[0] =~ /^([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/i },
171             );
172              
173             sub _validate_value
174             {
175 6     6   21 my( $value, $schema ) = @_;
176              
177 6 50       16 my $format = $schema->{format} if $schema;
178              
179             # If empty values are explicitly (dis)allowed, they are checked here.
180 6 50 66     32 if( $value eq '' && $schema && $schema->{allowEmptyValue} ) {
      66        
181 0 0       0 return $value if $schema->{allowEmptyValue} eq 'true';
182 0         0 return;
183             }
184              
185             # 'enum' is the strictest possible validation method.
186 6 50 33     31 if( $schema && $schema->{enum} ) {
187 0         0 return grep { $value eq $_ } @{$schema->{enum}};
  0         0  
  0         0  
188             }
189              
190             # 'pattern' is also quite strict.
191 6 50 33     23 if( $schema && $schema->{pattern} ) {
192 0 0       0 return $value =~ /^($schema->{pattern})$/ ? $1 : undef;
193             }
194              
195             # FIXME: Maybe employ a proper JSON Schema validator?
196             # Not sure if it untaints, though.
197 6 50 33     29 if( $format && exists $format_methods{$format} ) {
198 6         127 return $format_methods{$format}->( $value );
199             }
200              
201             # Tainted values may still get till here and are returned as such.
202 0           return $value;
203             }
204              
205             =head1 SEE ALSO
206              
207             L<https://spec.openapis.org/oas/v3.0.2.html>
208              
209             =cut
210              
211             1;