line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package RDF::ACL; |
2
|
|
|
|
|
|
|
|
3
|
5
|
|
|
5
|
|
135868
|
use 5.010; |
|
5
|
|
|
|
|
21
|
|
|
5
|
|
|
|
|
251
|
|
4
|
5
|
|
|
5
|
|
29
|
use strict; |
|
5
|
|
|
|
|
10
|
|
|
5
|
|
|
|
|
176
|
|
5
|
5
|
|
|
5
|
|
6040
|
use utf8; |
|
5
|
|
|
|
|
73
|
|
|
5
|
|
|
|
|
26
|
|
6
|
|
|
|
|
|
|
|
7
|
5
|
|
|
5
|
|
35006
|
use Data::UUID; |
|
5
|
|
|
|
|
187767
|
|
|
5
|
|
|
|
|
397
|
|
8
|
5
|
|
|
5
|
|
245704
|
use Error qw(:try); |
|
5
|
|
|
|
|
51089
|
|
|
5
|
|
|
|
|
34
|
|
9
|
5
|
|
|
5
|
|
9590
|
use RDF::TrineX::Functions -shortcuts; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
10
|
|
|
|
|
|
|
use RDF::Query; |
11
|
|
|
|
|
|
|
use RDF::Query::Client; |
12
|
|
|
|
|
|
|
use Scalar::Util qw(blessed); |
13
|
|
|
|
|
|
|
use URI; |
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
use constant EXCEPTION => 'Error::Simple'; |
16
|
|
|
|
|
|
|
use constant NS_ACL => 'http://www.w3.org/ns/auth/acl#'; |
17
|
|
|
|
|
|
|
use constant NS_RDF => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; |
18
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
our $AUTHORITY = 'cpan:TOBYINK'; |
20
|
|
|
|
|
|
|
our $VERSION = '0.104'; |
21
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
sub rdf_query |
23
|
|
|
|
|
|
|
{ |
24
|
|
|
|
|
|
|
my ($query, $model) = @_; |
25
|
|
|
|
|
|
|
my $class = (blessed($model) and $model->isa('RDF::Trine::Model')) |
26
|
|
|
|
|
|
|
? 'RDF::Query' |
27
|
|
|
|
|
|
|
: 'RDF::Query::Client'; |
28
|
|
|
|
|
|
|
my $results = $class->new($query)->execute($model); |
29
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
if ($results->is_boolean) |
31
|
|
|
|
|
|
|
{ return $results->get_boolean } |
32
|
|
|
|
|
|
|
if ($results->is_bindings) |
33
|
|
|
|
|
|
|
{ return $results } |
34
|
|
|
|
|
|
|
if ($results->is_graph) |
35
|
|
|
|
|
|
|
{ my $m = rdf_parse(); $m->add_hashref($results->as_hashref); return $m } |
36
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
return; |
38
|
|
|
|
|
|
|
} |
39
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
sub new |
41
|
|
|
|
|
|
|
{ |
42
|
|
|
|
|
|
|
my $class = shift; |
43
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
my $model = shift; |
45
|
|
|
|
|
|
|
unless (blessed($model) && $model->isa('RDF::Trine::Model')) |
46
|
|
|
|
|
|
|
{ |
47
|
|
|
|
|
|
|
$model = rdf_parse($model, @_); |
48
|
|
|
|
|
|
|
} |
49
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
my $self = bless { |
51
|
|
|
|
|
|
|
'model' => $model, |
52
|
|
|
|
|
|
|
'i_am' => undef , |
53
|
|
|
|
|
|
|
}, $class; |
54
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
return $self; |
56
|
|
|
|
|
|
|
} |
57
|
|
|
|
|
|
|
|
58
|
|
|
|
|
|
|
sub new_remote |
59
|
|
|
|
|
|
|
{ |
60
|
|
|
|
|
|
|
my $class = shift; |
61
|
|
|
|
|
|
|
my $ep = shift; |
62
|
|
|
|
|
|
|
|
63
|
|
|
|
|
|
|
my $self = bless { |
64
|
|
|
|
|
|
|
'endpoint' => $ep, |
65
|
|
|
|
|
|
|
}, $class; |
66
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
return $self; |
68
|
|
|
|
|
|
|
} |
69
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
sub check |
71
|
|
|
|
|
|
|
{ |
72
|
|
|
|
|
|
|
my ($self, $webid, $item, $level, @datas) = @_; |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
EXCEPTION->throw("Must provide WebID to be checked.") |
75
|
|
|
|
|
|
|
unless defined $webid; |
76
|
|
|
|
|
|
|
|
77
|
|
|
|
|
|
|
EXCEPTION->throw("Must provide item URI to be checked.") |
78
|
|
|
|
|
|
|
unless defined $item; |
79
|
|
|
|
|
|
|
|
80
|
|
|
|
|
|
|
my $model = $self->_union_model(@datas); |
81
|
|
|
|
|
|
|
|
82
|
|
|
|
|
|
|
my $aclvocab = NS_ACL; |
83
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
if (defined $level) |
85
|
|
|
|
|
|
|
{ |
86
|
|
|
|
|
|
|
if ($level =~ /^(access|read|write|control|append)$/i) |
87
|
|
|
|
|
|
|
{ |
88
|
|
|
|
|
|
|
$level = $aclvocab . (ucfirst lc $level); |
89
|
|
|
|
|
|
|
} |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
my $sparql = <<"SPARQL"; |
92
|
|
|
|
|
|
|
PREFIX acl: <$aclvocab> |
93
|
|
|
|
|
|
|
ASK WHERE { |
94
|
|
|
|
|
|
|
{ |
95
|
|
|
|
|
|
|
{ ?authorisation acl:agentClass ?agentclass . <$webid> a ?agentclass . } |
96
|
|
|
|
|
|
|
UNION { ?authorisation acl:agent <$webid> . } |
97
|
|
|
|
|
|
|
UNION { ?authorisation acl:agentClass <http://xmlns.com/foaf/0.1/Agent> . } |
98
|
|
|
|
|
|
|
} |
99
|
|
|
|
|
|
|
{ |
100
|
|
|
|
|
|
|
{ ?authorisation acl:accessToClass ?accessclass . <$item> a ?accessclass . } |
101
|
|
|
|
|
|
|
UNION { ?authorisation acl:accessTo <$item> . } |
102
|
|
|
|
|
|
|
UNION { ?authorisation acl:accessToClass <http://www.w3.org/2000/01/rdf-schema#Resource> . } |
103
|
|
|
|
|
|
|
} |
104
|
|
|
|
|
|
|
{ |
105
|
|
|
|
|
|
|
?authorisation acl:mode <$level> . |
106
|
|
|
|
|
|
|
} |
107
|
|
|
|
|
|
|
} |
108
|
|
|
|
|
|
|
SPARQL |
109
|
|
|
|
|
|
|
|
110
|
|
|
|
|
|
|
return rdf_query($sparql, $model); |
111
|
|
|
|
|
|
|
} |
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
else |
114
|
|
|
|
|
|
|
{ |
115
|
|
|
|
|
|
|
my $sparql = <<"SPARQL"; |
116
|
|
|
|
|
|
|
PREFIX acl: <$aclvocab> |
117
|
|
|
|
|
|
|
SELECT DISTINCT ?level |
118
|
|
|
|
|
|
|
WHERE { |
119
|
|
|
|
|
|
|
{ |
120
|
|
|
|
|
|
|
{ ?authorisation acl:agentClass ?agentclass . <$webid> a ?agentclass . } |
121
|
|
|
|
|
|
|
UNION { ?authorisation acl:agent <$webid> . } |
122
|
|
|
|
|
|
|
UNION { ?authorisation acl:agentClass <http://xmlns.com/foaf/0.1/Agent> . } |
123
|
|
|
|
|
|
|
} |
124
|
|
|
|
|
|
|
{ |
125
|
|
|
|
|
|
|
{ ?authorisation acl:accessToClass ?accessclass . <$item> a ?accessclass . } |
126
|
|
|
|
|
|
|
UNION { ?authorisation acl:accessTo <$item> . } |
127
|
|
|
|
|
|
|
UNION { ?authorisation acl:accessToClass <http://www.w3.org/2000/01/rdf-schema#Resource> . } |
128
|
|
|
|
|
|
|
} |
129
|
|
|
|
|
|
|
{ |
130
|
|
|
|
|
|
|
?authorisation acl:mode ?level . |
131
|
|
|
|
|
|
|
} |
132
|
|
|
|
|
|
|
} |
133
|
|
|
|
|
|
|
SPARQL |
134
|
|
|
|
|
|
|
|
135
|
|
|
|
|
|
|
my $iterator = rdf_query($sparql, $model); |
136
|
|
|
|
|
|
|
my @rv; |
137
|
|
|
|
|
|
|
while (my $result = $iterator->next) |
138
|
|
|
|
|
|
|
{ |
139
|
|
|
|
|
|
|
push @rv, $result->{'level'}->uri |
140
|
|
|
|
|
|
|
if blessed($result->{'level'}) && $result->{'level'}->can('uri'); |
141
|
|
|
|
|
|
|
} |
142
|
|
|
|
|
|
|
return @rv; |
143
|
|
|
|
|
|
|
} |
144
|
|
|
|
|
|
|
} |
145
|
|
|
|
|
|
|
|
146
|
|
|
|
|
|
|
sub why |
147
|
|
|
|
|
|
|
{ |
148
|
|
|
|
|
|
|
my ($self, $webid, $item, $level, @datas) = @_; |
149
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
EXCEPTION->throw("Must provide WebID to be checked.") |
151
|
|
|
|
|
|
|
unless defined $webid; |
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
EXCEPTION->throw("Must provide item URI to be checked.") |
154
|
|
|
|
|
|
|
unless defined $item; |
155
|
|
|
|
|
|
|
|
156
|
|
|
|
|
|
|
EXCEPTION->throw("Must provide item URI to be checked.") |
157
|
|
|
|
|
|
|
unless defined $item; |
158
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
my $model = $self->_union_model(@datas); |
160
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
my $aclvocab = NS_ACL; |
162
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
if ($level =~ /^(access|read|write|control|append)$/i) |
164
|
|
|
|
|
|
|
{ |
165
|
|
|
|
|
|
|
$level = $aclvocab . (ucfirst lc $level); |
166
|
|
|
|
|
|
|
} |
167
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
my $sparql = <<"SPARQL"; |
169
|
|
|
|
|
|
|
PREFIX acl: <$aclvocab> |
170
|
|
|
|
|
|
|
SELECT DISTINCT ?authorisation |
171
|
|
|
|
|
|
|
WHERE { |
172
|
|
|
|
|
|
|
{ |
173
|
|
|
|
|
|
|
{ ?authorisation acl:agentClass ?agentclass . <$webid> a ?agentclass . } |
174
|
|
|
|
|
|
|
UNION { ?authorisation acl:agent <$webid> . } |
175
|
|
|
|
|
|
|
UNION { ?authorisation acl:agentClass <http://xmlns.com/foaf/0.1/Agent> . } |
176
|
|
|
|
|
|
|
} |
177
|
|
|
|
|
|
|
{ |
178
|
|
|
|
|
|
|
{ ?authorisation acl:accessToClass ?accessclass . <$item> a ?accessclass . } |
179
|
|
|
|
|
|
|
UNION { ?authorisation acl:accessTo <$item> . } |
180
|
|
|
|
|
|
|
UNION { ?authorisation acl:accessToClass <http://www.w3.org/2000/01/rdf-schema#Resource> . } |
181
|
|
|
|
|
|
|
} |
182
|
|
|
|
|
|
|
{ |
183
|
|
|
|
|
|
|
?authorisation acl:mode <$level> . |
184
|
|
|
|
|
|
|
} |
185
|
|
|
|
|
|
|
} |
186
|
|
|
|
|
|
|
SPARQL |
187
|
|
|
|
|
|
|
|
188
|
|
|
|
|
|
|
my $iterator = rdf_query($sparql, $model); |
189
|
|
|
|
|
|
|
my @rv; |
190
|
|
|
|
|
|
|
while (my $result = $iterator->next) |
191
|
|
|
|
|
|
|
{ |
192
|
|
|
|
|
|
|
if (blessed($result->{'authorisation'}) && $result->{'authorisation'}->can('uri')) |
193
|
|
|
|
|
|
|
{ |
194
|
|
|
|
|
|
|
push @rv, $result->{'authorisation'}->uri; |
195
|
|
|
|
|
|
|
} |
196
|
|
|
|
|
|
|
else |
197
|
|
|
|
|
|
|
{ |
198
|
|
|
|
|
|
|
push @rv, undef; |
199
|
|
|
|
|
|
|
} |
200
|
|
|
|
|
|
|
} |
201
|
|
|
|
|
|
|
return @rv; |
202
|
|
|
|
|
|
|
} |
203
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
sub allow |
205
|
|
|
|
|
|
|
{ |
206
|
|
|
|
|
|
|
my ($self, %args) = @_; |
207
|
|
|
|
|
|
|
|
208
|
|
|
|
|
|
|
EXCEPTION->throw("This ACL is not mutable.") |
209
|
|
|
|
|
|
|
unless $self->is_mutable; |
210
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
EXCEPTION->throw("Must provide an 'item', 'item_class' or 'container' argument.") |
212
|
|
|
|
|
|
|
unless (defined $args{'item'} or defined $args{'item_class'} or defined $args{'container'}); |
213
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
EXCEPTION->throw("Cannot provide 'container' with an 'item' or 'item_class' argument.") |
215
|
|
|
|
|
|
|
if ((defined $args{'container'}) and (defined $args{'item'} or defined $args{'item_class'})); |
216
|
|
|
|
|
|
|
|
217
|
|
|
|
|
|
|
$args{'agent_class'} = 'http://xmlns.com/foaf/0.1/Agent' |
218
|
|
|
|
|
|
|
unless (defined $args{'webid'} or defined $args{'agent'} or defined $args{'agent_class'}); |
219
|
|
|
|
|
|
|
|
220
|
|
|
|
|
|
|
$args{'level'} = NS_ACL.'Read' |
221
|
|
|
|
|
|
|
unless defined $args{'level'}; |
222
|
|
|
|
|
|
|
|
223
|
|
|
|
|
|
|
my $predicate_map = { |
224
|
|
|
|
|
|
|
'level' => NS_ACL . 'mode' , |
225
|
|
|
|
|
|
|
'item' => NS_ACL . 'accessTo' , |
226
|
|
|
|
|
|
|
'item_class' => NS_ACL . 'accessToClass' , |
227
|
|
|
|
|
|
|
'container' => NS_ACL . 'defaultForNew' , |
228
|
|
|
|
|
|
|
'agent' => NS_ACL . 'agent' , |
229
|
|
|
|
|
|
|
'agent_class' => NS_ACL . 'agentClass' , |
230
|
|
|
|
|
|
|
'webid' => NS_ACL . 'agent' , |
231
|
|
|
|
|
|
|
}; |
232
|
|
|
|
|
|
|
|
233
|
|
|
|
|
|
|
my $data = {}; |
234
|
|
|
|
|
|
|
my $authid = $self->_uuid; |
235
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
$data->{$authid}->{NS_RDF.'type'} = [ |
237
|
|
|
|
|
|
|
{ 'type'=>'uri', 'value'=>NS_ACL.'Authorization' }, |
238
|
|
|
|
|
|
|
]; |
239
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
foreach my $p (keys %$predicate_map) |
241
|
|
|
|
|
|
|
{ |
242
|
|
|
|
|
|
|
next unless defined $args{$p}; |
243
|
|
|
|
|
|
|
|
244
|
|
|
|
|
|
|
unless (ref $args{$p} eq 'ARRAY') |
245
|
|
|
|
|
|
|
{ |
246
|
|
|
|
|
|
|
$args{$p} = [ $args{$p} ]; |
247
|
|
|
|
|
|
|
} |
248
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
foreach my $val (@{$args{$p}}) |
250
|
|
|
|
|
|
|
{ |
251
|
|
|
|
|
|
|
if (defined $self->who_am_i and $p =~ /^(item|container)$/) |
252
|
|
|
|
|
|
|
{ |
253
|
|
|
|
|
|
|
my $control = $self->check($self->who_am_i, $val, 'Control'); |
254
|
|
|
|
|
|
|
EXCEPTION->throw("WebID <".$self->who_am_i."> does not have access control for resource <$val>.") |
255
|
|
|
|
|
|
|
unless $control; |
256
|
|
|
|
|
|
|
} |
257
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
if ($p eq 'level' and $val =~ /^(access|read|write|control|append)$/i) |
259
|
|
|
|
|
|
|
{ |
260
|
|
|
|
|
|
|
$val = NS_ACL . (ucfirst lc $val); |
261
|
|
|
|
|
|
|
} |
262
|
|
|
|
|
|
|
|
263
|
|
|
|
|
|
|
push @{ $data->{$authid}->{$predicate_map->{$p}} }, |
264
|
|
|
|
|
|
|
{ 'type'=>'uri', 'value'=>$val }; |
265
|
|
|
|
|
|
|
} |
266
|
|
|
|
|
|
|
} |
267
|
|
|
|
|
|
|
|
268
|
|
|
|
|
|
|
$self->model->add_hashref($data); |
269
|
|
|
|
|
|
|
|
270
|
|
|
|
|
|
|
return $authid; |
271
|
|
|
|
|
|
|
} |
272
|
|
|
|
|
|
|
|
273
|
|
|
|
|
|
|
sub deny |
274
|
|
|
|
|
|
|
{ |
275
|
|
|
|
|
|
|
my ($self, $id) = @_; |
276
|
|
|
|
|
|
|
|
277
|
|
|
|
|
|
|
EXCEPTION->throw("This ACL is not mutable.") |
278
|
|
|
|
|
|
|
unless $self->is_mutable; |
279
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
if (defined $self->who_am_i) |
281
|
|
|
|
|
|
|
{ |
282
|
|
|
|
|
|
|
my $aclvocab = NS_ACL; |
283
|
|
|
|
|
|
|
my $sparql = <<"SPARQL"; |
284
|
|
|
|
|
|
|
PREFIX acl: <$aclvocab> |
285
|
|
|
|
|
|
|
SELECT DISTINCT ?resource |
286
|
|
|
|
|
|
|
WHERE |
287
|
|
|
|
|
|
|
{ |
288
|
|
|
|
|
|
|
{ <$id> acl:accessTo ?resource . } |
289
|
|
|
|
|
|
|
UNION { <$id> acl:accessTo ?resource . } |
290
|
|
|
|
|
|
|
} |
291
|
|
|
|
|
|
|
SPARQL |
292
|
|
|
|
|
|
|
my $iterator = rdf_query($sparql, $self->model); |
293
|
|
|
|
|
|
|
while (my $result = $iterator->next) |
294
|
|
|
|
|
|
|
{ |
295
|
|
|
|
|
|
|
next unless $result->{'resource'}->is_resource; |
296
|
|
|
|
|
|
|
next if $self->check($self->who_am_i, $result->{'resource'}->uri, 'Control'); |
297
|
|
|
|
|
|
|
|
298
|
|
|
|
|
|
|
EXCEPTION->throw("WebID <".$self->who_am_i."> does not have access control for resource <".$result->{'resource'}->uri.">."); |
299
|
|
|
|
|
|
|
} |
300
|
|
|
|
|
|
|
} |
301
|
|
|
|
|
|
|
|
302
|
|
|
|
|
|
|
my $auth = RDF::Trine::Node::Resource->new($id); |
303
|
|
|
|
|
|
|
my $count = $self->model->count_statements($auth, undef, undef); |
304
|
|
|
|
|
|
|
$self->model->remove_statements($auth, undef, undef); |
305
|
|
|
|
|
|
|
return $count; |
306
|
|
|
|
|
|
|
} |
307
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
sub created |
309
|
|
|
|
|
|
|
{ |
310
|
|
|
|
|
|
|
my ($self, $item, $container) = @_; |
311
|
|
|
|
|
|
|
|
312
|
|
|
|
|
|
|
EXCEPTION->throw("This ACL is not mutable.") |
313
|
|
|
|
|
|
|
unless $self->is_mutable; |
314
|
|
|
|
|
|
|
|
315
|
|
|
|
|
|
|
my $aclvocab = NS_ACL; |
316
|
|
|
|
|
|
|
my $graph = rdf_query(<<"QUERY", $self->model); |
317
|
|
|
|
|
|
|
PREFIX acl: <$aclvocab> |
318
|
|
|
|
|
|
|
CONSTRUCT { ?auth ?p ?o . } |
319
|
|
|
|
|
|
|
WHERE { |
320
|
|
|
|
|
|
|
?auth ?p ?o ; |
321
|
|
|
|
|
|
|
acl:defaultForNew <$container> . |
322
|
|
|
|
|
|
|
FILTER ( sameTerm(?p, acl:mode) || sameTerm(?p, acl:agent) || sameTerm(?p, acl:agentClass) || sameTerm(?p, <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>) ) |
323
|
|
|
|
|
|
|
} |
324
|
|
|
|
|
|
|
QUERY |
325
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
my $data = $graph->as_hashref; |
327
|
|
|
|
|
|
|
my $newdata = {}; |
328
|
|
|
|
|
|
|
my @rv; |
329
|
|
|
|
|
|
|
foreach my $k (keys %$data) |
330
|
|
|
|
|
|
|
{ |
331
|
|
|
|
|
|
|
my $authid = $self->_uuid; |
332
|
|
|
|
|
|
|
$newdata->{$authid} = $data->{$k}; |
333
|
|
|
|
|
|
|
$newdata->{$authid}->{$aclvocab.'accessTo'} = [{ |
334
|
|
|
|
|
|
|
'type' => 'uri', 'value' => $item |
335
|
|
|
|
|
|
|
}]; |
336
|
|
|
|
|
|
|
push @rv, $authid; |
337
|
|
|
|
|
|
|
} |
338
|
|
|
|
|
|
|
$self->model->add_hashref($newdata); |
339
|
|
|
|
|
|
|
|
340
|
|
|
|
|
|
|
return @rv; |
341
|
|
|
|
|
|
|
} |
342
|
|
|
|
|
|
|
|
343
|
|
|
|
|
|
|
sub i_am |
344
|
|
|
|
|
|
|
{ |
345
|
|
|
|
|
|
|
my $self = shift; |
346
|
|
|
|
|
|
|
my $old = $self->who_am_i; |
347
|
|
|
|
|
|
|
$self->{'i_am'} = shift; |
348
|
|
|
|
|
|
|
return URI->new($old); |
349
|
|
|
|
|
|
|
} |
350
|
|
|
|
|
|
|
|
351
|
|
|
|
|
|
|
sub who_am_i |
352
|
|
|
|
|
|
|
{ |
353
|
|
|
|
|
|
|
my ($self) = @_; |
354
|
|
|
|
|
|
|
return $self->{'i_am'}; |
355
|
|
|
|
|
|
|
} |
356
|
|
|
|
|
|
|
|
357
|
|
|
|
|
|
|
sub save |
358
|
|
|
|
|
|
|
{ |
359
|
|
|
|
|
|
|
my ($self, $fmt, $file) = @_; |
360
|
|
|
|
|
|
|
|
361
|
|
|
|
|
|
|
EXCEPTION->throw("This ACL is not serialisable.") |
362
|
|
|
|
|
|
|
if $self->is_remote; |
363
|
|
|
|
|
|
|
|
364
|
|
|
|
|
|
|
return rdf_string( |
365
|
|
|
|
|
|
|
$self->model, |
366
|
|
|
|
|
|
|
type => $fmt, |
367
|
|
|
|
|
|
|
output => $file, |
368
|
|
|
|
|
|
|
); |
369
|
|
|
|
|
|
|
} |
370
|
|
|
|
|
|
|
|
371
|
|
|
|
|
|
|
sub is_remote |
372
|
|
|
|
|
|
|
{ |
373
|
|
|
|
|
|
|
my ($self) = @_; |
374
|
|
|
|
|
|
|
return defined $self->endpoint; |
375
|
|
|
|
|
|
|
} |
376
|
|
|
|
|
|
|
|
377
|
|
|
|
|
|
|
sub is_mutable |
378
|
|
|
|
|
|
|
{ |
379
|
|
|
|
|
|
|
my ($self) = @_; |
380
|
|
|
|
|
|
|
return defined $self->model; |
381
|
|
|
|
|
|
|
} |
382
|
|
|
|
|
|
|
|
383
|
|
|
|
|
|
|
sub model |
384
|
|
|
|
|
|
|
{ |
385
|
|
|
|
|
|
|
my ($self) = @_; |
386
|
|
|
|
|
|
|
return $self->{'model'}; |
387
|
|
|
|
|
|
|
} |
388
|
|
|
|
|
|
|
|
389
|
|
|
|
|
|
|
sub endpoint |
390
|
|
|
|
|
|
|
{ |
391
|
|
|
|
|
|
|
my ($self) = @_; |
392
|
|
|
|
|
|
|
return undef unless defined $self->{'endpoint'}; |
393
|
|
|
|
|
|
|
return URI->new(''.$self->{'endpoint'}); |
394
|
|
|
|
|
|
|
} |
395
|
|
|
|
|
|
|
|
396
|
|
|
|
|
|
|
# PRIVATE METHODS |
397
|
|
|
|
|
|
|
|
398
|
|
|
|
|
|
|
# * $acl->_uuid |
399
|
|
|
|
|
|
|
# |
400
|
|
|
|
|
|
|
# Returns a unique throwaway URI. |
401
|
|
|
|
|
|
|
|
402
|
|
|
|
|
|
|
sub _uuid |
403
|
|
|
|
|
|
|
{ |
404
|
|
|
|
|
|
|
my ($self) = @_; |
405
|
|
|
|
|
|
|
|
406
|
|
|
|
|
|
|
$self->{'uuid_generator'} = Data::UUID->new |
407
|
|
|
|
|
|
|
unless defined $self->{'uuid_generator'}; |
408
|
|
|
|
|
|
|
|
409
|
|
|
|
|
|
|
return 'urn:uuid:' . $self->{'uuid_generator'}->create_str; |
410
|
|
|
|
|
|
|
} |
411
|
|
|
|
|
|
|
|
412
|
|
|
|
|
|
|
# * $acl->_union_model(@graphs) |
413
|
|
|
|
|
|
|
# |
414
|
|
|
|
|
|
|
# Creates a temporary model that is the union of the ACL |
415
|
|
|
|
|
|
|
# object's default data source plus additional graphs. |
416
|
|
|
|
|
|
|
|
417
|
|
|
|
|
|
|
sub _union_model |
418
|
|
|
|
|
|
|
{ |
419
|
|
|
|
|
|
|
my ($self, @graphs) = @_; |
420
|
|
|
|
|
|
|
my $model; |
421
|
|
|
|
|
|
|
|
422
|
|
|
|
|
|
|
if ($self->is_remote) |
423
|
|
|
|
|
|
|
{ |
424
|
|
|
|
|
|
|
$model = $self->endpoint; |
425
|
|
|
|
|
|
|
|
426
|
|
|
|
|
|
|
EXCEPTION->throw("Cannot provide additional data to consider for remote ACL.") |
427
|
|
|
|
|
|
|
if @graphs; |
428
|
|
|
|
|
|
|
} |
429
|
|
|
|
|
|
|
elsif (@graphs) |
430
|
|
|
|
|
|
|
{ |
431
|
|
|
|
|
|
|
$model = rdf_parse($self->model, model => RDF::TrineX::Functions::model()); |
432
|
|
|
|
|
|
|
foreach my $given (@graphs) |
433
|
|
|
|
|
|
|
{ |
434
|
|
|
|
|
|
|
my @given = ref($given) eq 'ARRAY' ? @$given : $given; |
435
|
|
|
|
|
|
|
rdf_parse(@given, model => $model); |
436
|
|
|
|
|
|
|
} |
437
|
|
|
|
|
|
|
} |
438
|
|
|
|
|
|
|
else |
439
|
|
|
|
|
|
|
{ |
440
|
|
|
|
|
|
|
$model = $self->model; |
441
|
|
|
|
|
|
|
} |
442
|
|
|
|
|
|
|
|
443
|
|
|
|
|
|
|
return $model; |
444
|
|
|
|
|
|
|
} |
445
|
|
|
|
|
|
|
|
446
|
|
|
|
|
|
|
__PACKAGE__ |
447
|
|
|
|
|
|
|
__END__ |
448
|
|
|
|
|
|
|
|
449
|
|
|
|
|
|
|
=head1 NAME |
450
|
|
|
|
|
|
|
|
451
|
|
|
|
|
|
|
RDF::ACL - access control lists for the semantic web |
452
|
|
|
|
|
|
|
|
453
|
|
|
|
|
|
|
=head1 SYNOPSIS |
454
|
|
|
|
|
|
|
|
455
|
|
|
|
|
|
|
use RDF::ACL; |
456
|
|
|
|
|
|
|
|
457
|
|
|
|
|
|
|
my $acl = RDF::ACL->new('access.ttl'); |
458
|
|
|
|
|
|
|
my $auth = $acl->allow( |
459
|
|
|
|
|
|
|
webid => 'http://example.com/joe#me', |
460
|
|
|
|
|
|
|
item => 'http://example.com/private/document', |
461
|
|
|
|
|
|
|
level => ['Read', 'Write'], |
462
|
|
|
|
|
|
|
); |
463
|
|
|
|
|
|
|
$acl->save('turtle', 'access.ttl'); |
464
|
|
|
|
|
|
|
|
465
|
|
|
|
|
|
|
# later... |
466
|
|
|
|
|
|
|
|
467
|
|
|
|
|
|
|
if ($acl->check('http://example.com/joe#me', |
468
|
|
|
|
|
|
|
'http://example.com/private/document', |
469
|
|
|
|
|
|
|
'Read')) |
470
|
|
|
|
|
|
|
{ |
471
|
|
|
|
|
|
|
print slurp("private/document"); |
472
|
|
|
|
|
|
|
} |
473
|
|
|
|
|
|
|
else |
474
|
|
|
|
|
|
|
{ |
475
|
|
|
|
|
|
|
print "Denied"; |
476
|
|
|
|
|
|
|
} |
477
|
|
|
|
|
|
|
|
478
|
|
|
|
|
|
|
# later... |
479
|
|
|
|
|
|
|
|
480
|
|
|
|
|
|
|
foreach my $reason ($acl->why('http://example.com/joe#me', |
481
|
|
|
|
|
|
|
'http://example.com/private/document', |
482
|
|
|
|
|
|
|
'Read')) |
483
|
|
|
|
|
|
|
{ |
484
|
|
|
|
|
|
|
$acl->deny($reason) if defined $reason; |
485
|
|
|
|
|
|
|
} |
486
|
|
|
|
|
|
|
$acl->save('turtle', 'access.ttl'); |
487
|
|
|
|
|
|
|
|
488
|
|
|
|
|
|
|
=head1 DESCRIPTION |
489
|
|
|
|
|
|
|
|
490
|
|
|
|
|
|
|
Note that this module provides access control and does not perform |
491
|
|
|
|
|
|
|
authentication! |
492
|
|
|
|
|
|
|
|
493
|
|
|
|
|
|
|
=head2 Constructors |
494
|
|
|
|
|
|
|
|
495
|
|
|
|
|
|
|
=over 4 |
496
|
|
|
|
|
|
|
|
497
|
|
|
|
|
|
|
=item C<< $acl->new($input, %args) >> |
498
|
|
|
|
|
|
|
|
499
|
|
|
|
|
|
|
Creates a new access control list based on RDF data defined in |
500
|
|
|
|
|
|
|
$input. $input can be a serialised string of RDF, a file name, |
501
|
|
|
|
|
|
|
a URI or any other input accepted by the C<parse> function |
502
|
|
|
|
|
|
|
of L<RDF::TrineX::Functions>. |
503
|
|
|
|
|
|
|
|
504
|
|
|
|
|
|
|
C<< new() >> can be called with no arguments to create a |
505
|
|
|
|
|
|
|
fresh, clean ACL containing no authorisations. |
506
|
|
|
|
|
|
|
|
507
|
|
|
|
|
|
|
=item C<< $acl->new_remote($endpoint) >> |
508
|
|
|
|
|
|
|
|
509
|
|
|
|
|
|
|
Creates a new access control list based on RDF data accessed |
510
|
|
|
|
|
|
|
via a remote SPARQL Protocol 1.0 endpoint. |
511
|
|
|
|
|
|
|
|
512
|
|
|
|
|
|
|
=back |
513
|
|
|
|
|
|
|
|
514
|
|
|
|
|
|
|
=head2 Public Methods |
515
|
|
|
|
|
|
|
|
516
|
|
|
|
|
|
|
=over 4 |
517
|
|
|
|
|
|
|
|
518
|
|
|
|
|
|
|
=item C<< $acl->check($webid, $item, $level, @data) >> |
519
|
|
|
|
|
|
|
|
520
|
|
|
|
|
|
|
Checks an agent's authorisation to access an item. |
521
|
|
|
|
|
|
|
|
522
|
|
|
|
|
|
|
$webid is the WebID (URI) of the agent requesting access to the item. |
523
|
|
|
|
|
|
|
|
524
|
|
|
|
|
|
|
$item is the URL (URI) of the item being accessed. |
525
|
|
|
|
|
|
|
|
526
|
|
|
|
|
|
|
$level is a URI identifying the type of access required. As special |
527
|
|
|
|
|
|
|
cases, the case-insensitive string 'read' is expanded to the URI |
528
|
|
|
|
|
|
|
E<lt>http://www.w3.org/ns/auth/acl#ReadE<gt>, 'write' to |
529
|
|
|
|
|
|
|
E<lt>http://www.w3.org/ns/auth/acl#WriteE<gt>, 'append' to |
530
|
|
|
|
|
|
|
E<lt>http://www.w3.org/ns/auth/acl#AppendE<gt> and 'control' to |
531
|
|
|
|
|
|
|
E<lt>http://www.w3.org/ns/auth/acl#ControlE<gt>. |
532
|
|
|
|
|
|
|
|
533
|
|
|
|
|
|
|
If the access control list is local (not remote), zero or more |
534
|
|
|
|
|
|
|
additional RDF graphs can be passed (i.e. @data) containing |
535
|
|
|
|
|
|
|
data to take into consideration when checking the agent's authorisation. |
536
|
|
|
|
|
|
|
This data is trusted blindly, so should not include data that the |
537
|
|
|
|
|
|
|
user has themselves supplied. If the access control list is remote, |
538
|
|
|
|
|
|
|
then this method throws an error if any additional data is provided. |
539
|
|
|
|
|
|
|
(A remote ACL cannot take into account local data.) |
540
|
|
|
|
|
|
|
|
541
|
|
|
|
|
|
|
If $level is provided, this method returns a boolean. |
542
|
|
|
|
|
|
|
|
543
|
|
|
|
|
|
|
If $level is undefined or omitted, this method returns a list |
544
|
|
|
|
|
|
|
of URIs which each represent a type of access that the user is |
545
|
|
|
|
|
|
|
authorised. |
546
|
|
|
|
|
|
|
|
547
|
|
|
|
|
|
|
=item C<< $acl->why($webid, $item, $level, @data) >> |
548
|
|
|
|
|
|
|
|
549
|
|
|
|
|
|
|
Investigates an agent's authorisation to access an item. |
550
|
|
|
|
|
|
|
|
551
|
|
|
|
|
|
|
Arguments as per C<< check >>, however $level is required. |
552
|
|
|
|
|
|
|
|
553
|
|
|
|
|
|
|
Returns a list of authorisations that justify a user's access to |
554
|
|
|
|
|
|
|
the item with the given access level. These authorisations are |
555
|
|
|
|
|
|
|
equivalent to $authid values provided by C<< allow() >>. |
556
|
|
|
|
|
|
|
|
557
|
|
|
|
|
|
|
In some cases (especially if the authorisation was created |
558
|
|
|
|
|
|
|
by hand, and not via C<< allow() >>) an authorisation may not |
559
|
|
|
|
|
|
|
have an identifier. In these cases, the list will contain |
560
|
|
|
|
|
|
|
undef. |
561
|
|
|
|
|
|
|
|
562
|
|
|
|
|
|
|
=item C<< $acl->allow(%args) >> |
563
|
|
|
|
|
|
|
|
564
|
|
|
|
|
|
|
Adds an authorisation to the ACL. The ACL must be mutable. |
565
|
|
|
|
|
|
|
|
566
|
|
|
|
|
|
|
The method takes a hash of named arguments: |
567
|
|
|
|
|
|
|
|
568
|
|
|
|
|
|
|
my $authid = $acl->allow( |
569
|
|
|
|
|
|
|
webid => 'http://example.com/joe#me', |
570
|
|
|
|
|
|
|
item => 'http://example.com/private/document', |
571
|
|
|
|
|
|
|
level => ['Read', 'Write'], |
572
|
|
|
|
|
|
|
); |
573
|
|
|
|
|
|
|
|
574
|
|
|
|
|
|
|
'item' is the URI of the item to authorise access to. As an alternative, |
575
|
|
|
|
|
|
|
'item_class' may be used to authorise access to an entire class of items |
576
|
|
|
|
|
|
|
(using classes in the RDFS/OWL sense of the word). If neither of these |
577
|
|
|
|
|
|
|
arguments is provided, then the method will throw an error. Both may be |
578
|
|
|
|
|
|
|
provided. Either or both may be an arrayref, because an authorisation |
579
|
|
|
|
|
|
|
may authorise access to more than one thing. |
580
|
|
|
|
|
|
|
|
581
|
|
|
|
|
|
|
'container' is an alternative to using 'item' or 'item_class'. It |
582
|
|
|
|
|
|
|
specifies the URI for a resource which in some way is a container for |
583
|
|
|
|
|
|
|
other resources. Setting authorisations for a container allows you |
584
|
|
|
|
|
|
|
to set a default authorisation for new items created within that |
585
|
|
|
|
|
|
|
container. (You must use the C<< created() >> method to notify the ACL |
586
|
|
|
|
|
|
|
about newly created items.) |
587
|
|
|
|
|
|
|
|
588
|
|
|
|
|
|
|
'webid' is the WebID (URI) of the person or agent being granted access. |
589
|
|
|
|
|
|
|
As an alternative, 'agent_class' may be used to authorise access to an |
590
|
|
|
|
|
|
|
entire class of agents. If neither is provided, an agent_class of |
591
|
|
|
|
|
|
|
E<lt>http://xmlns.com/foaf/0.1/AgentE<gt> is assumed. Both may be |
592
|
|
|
|
|
|
|
provided. Either or both may be an arrayref, because an authorisation |
593
|
|
|
|
|
|
|
may authorise access by more than one agent. (For consistency with 'item', |
594
|
|
|
|
|
|
|
'agent' is supported as a synonym for 'webid'.) |
595
|
|
|
|
|
|
|
|
596
|
|
|
|
|
|
|
'level' is the access level being granted. As with the C<< check >> |
597
|
|
|
|
|
|
|
method, the shortcuts 'read', 'write', 'append' and 'control' may be used. |
598
|
|
|
|
|
|
|
An arrayref may be used. If no level is specified, 'read' is assumed. |
599
|
|
|
|
|
|
|
|
600
|
|
|
|
|
|
|
This authorisation is not automatically saved, so it is probably useful |
601
|
|
|
|
|
|
|
to call C<< save() >> after adding authorisations. |
602
|
|
|
|
|
|
|
|
603
|
|
|
|
|
|
|
The method returns an identifier for the authorisation. This identifier |
604
|
|
|
|
|
|
|
may be needed again if you ever need to C<< deny() >> the authorisation. |
605
|
|
|
|
|
|
|
|
606
|
|
|
|
|
|
|
This method is aware of C<< i_am() >>/C<< who_am_i() >>. |
607
|
|
|
|
|
|
|
|
608
|
|
|
|
|
|
|
=item C<< $acl->deny($authid) >> |
609
|
|
|
|
|
|
|
|
610
|
|
|
|
|
|
|
Completely removes all traces of an authorisation from the ACL. |
611
|
|
|
|
|
|
|
|
612
|
|
|
|
|
|
|
The authorisation identifier can be found using C<< why() >> or |
613
|
|
|
|
|
|
|
you may have remembered it when you first allowed the access. |
614
|
|
|
|
|
|
|
In some cases (especially if the authorisation was created |
615
|
|
|
|
|
|
|
by hand, and not via C<< allow() >>) an authorisation may not |
616
|
|
|
|
|
|
|
have an identifier. In these cases, you will have to be creative |
617
|
|
|
|
|
|
|
in figuring out how to deny access. |
618
|
|
|
|
|
|
|
|
619
|
|
|
|
|
|
|
Returns the number of statements removed from the ACL's internal model |
620
|
|
|
|
|
|
|
as a result of the removal. (This will normally be at least 3.) |
621
|
|
|
|
|
|
|
|
622
|
|
|
|
|
|
|
This authorisation is not automatically saved, so it is probably useful |
623
|
|
|
|
|
|
|
to call C<< save() >> after removing authorisations. |
624
|
|
|
|
|
|
|
|
625
|
|
|
|
|
|
|
This method is aware of C<< i_am() >>/C<< who_am_i() >>. |
626
|
|
|
|
|
|
|
|
627
|
|
|
|
|
|
|
=item C<< $acl->created($item, $container) >> |
628
|
|
|
|
|
|
|
|
629
|
|
|
|
|
|
|
Finds all authorisations which are the default for new items within |
630
|
|
|
|
|
|
|
$container and clones each of them for newly created $item. |
631
|
|
|
|
|
|
|
|
632
|
|
|
|
|
|
|
Returns a list of authorisation identifiers. |
633
|
|
|
|
|
|
|
|
634
|
|
|
|
|
|
|
=item C<< $acl->i_am($webid) >> |
635
|
|
|
|
|
|
|
|
636
|
|
|
|
|
|
|
Tells the ACL object to "act like" the agent with the given WebID. |
637
|
|
|
|
|
|
|
|
638
|
|
|
|
|
|
|
If the ACL object is acting like you, then methods that make changes |
639
|
|
|
|
|
|
|
to the ACL (e.g. C<< allow() >> and C<< deny() >>) will only work |
640
|
|
|
|
|
|
|
if you have 'Control' permission over the resources specified. |
641
|
|
|
|
|
|
|
|
642
|
|
|
|
|
|
|
$webid can be null to restore the usual behaviour. |
643
|
|
|
|
|
|
|
|
644
|
|
|
|
|
|
|
Returns the previous WebID the ACL was acting like as a L<URI> |
645
|
|
|
|
|
|
|
object. |
646
|
|
|
|
|
|
|
|
647
|
|
|
|
|
|
|
=item C<< $acl->who_am_i >> |
648
|
|
|
|
|
|
|
|
649
|
|
|
|
|
|
|
Returns the WebID of the agent that ACL is acting like (if any). |
650
|
|
|
|
|
|
|
|
651
|
|
|
|
|
|
|
=item C<< $acl->save($format, $filename) >> |
652
|
|
|
|
|
|
|
|
653
|
|
|
|
|
|
|
Serialises a local (not remote) ACL. |
654
|
|
|
|
|
|
|
|
655
|
|
|
|
|
|
|
$format can be any format supported by the C<serialize> function from |
656
|
|
|
|
|
|
|
L<RDF::TrineX::Functions>. |
657
|
|
|
|
|
|
|
|
658
|
|
|
|
|
|
|
If $filename is provided, this method writes to the file |
659
|
|
|
|
|
|
|
and returns the new file size in bytes. |
660
|
|
|
|
|
|
|
|
661
|
|
|
|
|
|
|
If $filename is omitted, this method does not attempt to write |
662
|
|
|
|
|
|
|
to a file, and simply returns the string it would have written. |
663
|
|
|
|
|
|
|
|
664
|
|
|
|
|
|
|
=item C<< $acl->is_remote >> |
665
|
|
|
|
|
|
|
|
666
|
|
|
|
|
|
|
Returns true if the ACL is remote; false if local. |
667
|
|
|
|
|
|
|
|
668
|
|
|
|
|
|
|
=item C<< $acl->is_mutable >> |
669
|
|
|
|
|
|
|
|
670
|
|
|
|
|
|
|
Can this ACL be modified? |
671
|
|
|
|
|
|
|
|
672
|
|
|
|
|
|
|
=item C<< $acl->model >> |
673
|
|
|
|
|
|
|
|
674
|
|
|
|
|
|
|
The graph model against which authorisation checks are made. |
675
|
|
|
|
|
|
|
|
676
|
|
|
|
|
|
|
Returned as an L<RDF::Trine::Model> object. |
677
|
|
|
|
|
|
|
|
678
|
|
|
|
|
|
|
=item C<< $acl->endpoint >> |
679
|
|
|
|
|
|
|
|
680
|
|
|
|
|
|
|
The endpoint URI for remote (non-local) ACL queries. |
681
|
|
|
|
|
|
|
|
682
|
|
|
|
|
|
|
Returned as a L<URI> object. |
683
|
|
|
|
|
|
|
|
684
|
|
|
|
|
|
|
=back |
685
|
|
|
|
|
|
|
|
686
|
|
|
|
|
|
|
=head1 BUGS |
687
|
|
|
|
|
|
|
|
688
|
|
|
|
|
|
|
Please report any bugs to L<http://rt.cpan.org/>. |
689
|
|
|
|
|
|
|
|
690
|
|
|
|
|
|
|
=head1 SEE ALSO |
691
|
|
|
|
|
|
|
|
692
|
|
|
|
|
|
|
L<Web::ID>. |
693
|
|
|
|
|
|
|
|
694
|
|
|
|
|
|
|
L<http://www.w3.org/ns/auth/acl.n3>. |
695
|
|
|
|
|
|
|
|
696
|
|
|
|
|
|
|
L<http://www.perlrdf.org/>, L<http://lists.foaf-project.org/mailman/listinfo/foaf-protocols>. |
697
|
|
|
|
|
|
|
|
698
|
|
|
|
|
|
|
=head1 AUTHOR |
699
|
|
|
|
|
|
|
|
700
|
|
|
|
|
|
|
Toby Inkster E<lt>tobyink@cpan.orgE<gt>. |
701
|
|
|
|
|
|
|
|
702
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENCE |
703
|
|
|
|
|
|
|
|
704
|
|
|
|
|
|
|
This software is copyright (c) 2010-2013 by Toby Inkster. |
705
|
|
|
|
|
|
|
|
706
|
|
|
|
|
|
|
This is free software; you can redistribute it and/or modify it under |
707
|
|
|
|
|
|
|
the same terms as the Perl 5 programming language system itself. |
708
|
|
|
|
|
|
|
|
709
|
|
|
|
|
|
|
=head1 DISCLAIMER OF WARRANTIES |
710
|
|
|
|
|
|
|
|
711
|
|
|
|
|
|
|
THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED |
712
|
|
|
|
|
|
|
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF |
713
|
|
|
|
|
|
|
MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. |
714
|
|
|
|
|
|
|
|
715
|
|
|
|
|
|
|
|