File: AmazonS3.pm

package info (click to toggle)
request-tracker5 5.0.7%2Bdfsg-6
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 80,264 kB
  • sloc: javascript: 191,898; perl: 87,146; sh: 1,426; makefile: 487; python: 37; php: 15
file content (348 lines) | stat: -rw-r--r-- 10,033 bytes parent folder | download | duplicates (2)
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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# BEGIN BPS TAGGED BLOCK {{{
#
# COPYRIGHT:
#
# This software is Copyright (c) 1996-2024 Best Practical Solutions, LLC
#                                          <sales@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
#
#
# LICENSE:
#
# This work is made available to you under the terms of Version 2 of
# the GNU General Public License. A copy of that license should have
# been provided with this software, but in any event can be snarfed
# from www.gnu.org.
#
# This work is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 or visit their web page on the internet at
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
#
#
# CONTRIBUTION SUBMISSION POLICY:
#
# (The following paragraph is not intended to limit the rights granted
# to you to modify and distribute this software under the terms of
# the GNU General Public License and is only of importance to you if
# you choose to contribute your changes and enhancements to the
# community by submitting them to Best Practical Solutions, LLC.)
#
# By intentionally submitting any modifications, corrections or
# derivatives to this work, or any other work intended for use with
# Request Tracker, to Best Practical Solutions, LLC, you confirm that
# you are the copyright holder for those contributions and you grant
# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
# royalty-free, perpetual, license to use, copy, create derivative
# works based on those contributions, and sublicense and distribute
# those contributions and any derivatives thereof.
#
# END BPS TAGGED BLOCK }}}

use warnings;
use strict;

package RT::ExternalStorage::AmazonS3;

use Role::Basic qw/with/;
with 'RT::ExternalStorage::Backend';

sub S3 {
    my $self = shift;
    if (@_) {
        $self->{S3} = shift;
    }
    return $self->{S3};
}

sub Bucket {
    my $self = shift;
    return $self->{Bucket};
}

sub AccessKeyId {
    my $self = shift;
    return $self->{AccessKeyId};
}

sub SecretAccessKey {
    my $self = shift;
    return $self->{SecretAccessKey};
}

sub BucketObj {
    my $self = shift;
    return $self->S3->bucket($self->Bucket);
}

sub Init {
    my $self = shift;

    if (not RT::StaticUtil::RequireModule("Amazon::S3")) {
        RT->Logger->error("Required module Amazon::S3 is not installed");
        return;
    }

    for my $key (qw/AccessKeyId SecretAccessKey Bucket/) {
        if (not $self->$key) {
            RT->Logger->error("Required option '$key' not provided for AmazonS3 external storage. See the documentation for " . __PACKAGE__ . " for setting up this integration.");
            return;
        }
    }

    my %args = (
        aws_access_key_id     => $self->AccessKeyId,
        aws_secret_access_key => $self->SecretAccessKey,
        retry                 => 1,
    );
    $args{host}   = $self->{Host} if $self->{Host};
    $args{region} = $self->{Region} if $self->{Region};

    my $S3 = Amazon::S3->new(\%args);
    $self->S3($S3);

    my $buckets = $S3->bucket( $self->Bucket );
    unless ( $buckets ) {
        RT->Logger->error("Can't list buckets of AmazonS3: ".$S3->errstr);
        return;
    }

    my @buckets = $buckets->{buckets} ? @{$buckets->{buckets}} : ($buckets);

    unless ( grep {$_->bucket eq $self->Bucket} @buckets ) {
        my $ok = $S3->add_bucket( {
            bucket    => $self->Bucket,
            acl_short => 'private',
        } );
        if ($ok) {
            RT->Logger->debug("Created new bucket '".$self->Bucket."' on AmazonS3");
        }
        else {
            RT->Logger->error("Can't create new bucket '".$self->Bucket."' on AmazonS3: ".$S3->errstr);
            return;
        }
    }

    return $self;
}

sub Get {
    my $self = shift;
    my ($sha) = @_;

    my $ok = $self->BucketObj->get_key( $sha );
    return (undef, "Could not retrieve from AmazonS3:" . $self->S3->errstr)
        unless $ok;
    return ($ok->{value});
}

sub Store {
    my $self = shift;
    my ($sha, $content, $attachment) = @_;

    # No-op if the path exists already
    return ($sha) if $self->BucketObj->head_key( $sha );

    # Without content_type, S3 can guess wrong and cause attachments downloaded
    # via a link to have a content type of binary/octet-stream
    $self->BucketObj->add_key(
        $sha => $content,
        { content_type => $attachment->ContentType }
    ) or return (undef, "Failed to write to AmazonS3: " . $self->S3->errstr);

    return ($sha);
}

sub DownloadURLFor {
    my $self = shift;
    my $object = shift;

    my $column = $object->isa('RT::Attachment') ? 'Content' : 'LargeContent';
    my $digest = $object->__Value($column);

    # "If you make a request to the http://BUCKET.s3.amazonaws.com
    # endpoint, the DNS has sufficient information to route your request
    # directly to the region where your bucket resides."
    return "https://" . $self->Bucket . ".s3.amazonaws.com/" . $digest;
}

=head1 NAME

RT::ExternalStorage::AmazonS3 - Store files in Amazon's S3 cloud

=head1 SYNOPSIS

    Set(%ExternalStorage,
        Type            => 'AmazonS3',
        AccessKeyId     => '...',
        SecretAccessKey => '...',
        Bucket          => '...',
    );

=head1 DESCRIPTION

This storage option places attachments in the S3 cloud file storage
service.  The files are de-duplicated when they are saved; as such, if
the same file appears in multiple transactions, only one copy will be
stored in S3.

Files in S3 B<must not be modified or removed>; doing so may cause
internal inconsistency.  It is also important to ensure that the S3
account used maintains sufficient funds for your RT's B<storage and
bandwidth> needs.

=head1 SETUP

In order to use this storage type, you must grant RT access to your
S3 account.

=over

=item 1.

Log into Amazon S3, L<https://aws.amazon.com/s3/>, as the account you wish
to store files under.

=item 2.

Navigate to "Security Credentials" under your account name in the menu bar.

=item 3.

Open the "Access Keys" pane.

=item 4.

Click "Create New Access Key".

=item 5.

Copy the provided values for Access Key ID and Secret Access Key into
 your F<RT_SiteConfig.pm> file:

    Set(%ExternalStorage,
        Type            => 'AmazonS3',
        AccessKeyId     => '...', # Put Access Key ID between quotes
        SecretAccessKey => '...', # Put Secret Access Key between quotes
        Bucket          => '...',
    );

=item 6.

Set up a Bucket for RT to use. You can either create and configure it
in the S3 web interface, or let RT create one itself. Either way, tell
RT what bucket name to use in your F<RT_SiteConfig.pm> file:

    Set(%ExternalStorage,
        Type            => 'AmazonS3',
        AccessKeyId     => '...',
        SecretAccessKey => '...',
        Bucket          => '...', # Put bucket name between quotes
    );

=back

=head1 CONFIGURATION

The following additional configuration options have defaults, but can
be set to custom values.

=over

=item C<Host>

The S3 host endpoint to connect to.

The default from L<Amazon::S3> is C<s3.amazonaws.com>.

=item C<Region>

The AWS region where the S3 bucket is located.

The default from L<Amazon::S3> is us-east-1.

=back

=head2 Direct Linking

This storage engine supports direct linking. This means that RT can link
I<directly> to S3 when listing attachments, showing image previews, etc.
This relieves some bandwidth pressure from RT because ordinarily it would
have to download each attachment from S3 to be able to serve it.

To enable direct linking you must first make all content in your bucket
publicly viewable.

B<Beware that this could have serious implications for billing and
privacy>. RT cannot enforce its access controls for content on S3. This
is tempered somewhat by the fact that users must be able to guess the
SHA-256 digest of the file to be able to access it. But there is nothing
stopping someone from tweeting a URL to a file hosted on your S3. These
concerns do not arise when using an RT-mediated link to S3, since RT
uses an access key to upload to and download from S3.

To make all content in an S3 bucket publicly viewable, navigate to
the bucket in the S3 web UI. Select the "Properties" tab and inside
"Permissions" there is a button to "Add bucket policy". Paste the
following content in the provided textbox:

    {
        "Version": "2008-10-17",
        "Statement": [
            {
                "Sid": "AllowPublicRead",
                "Effect": "Allow",
                "Principal": {
                    "AWS": "*"
                },
                "Action": "s3:GetObject",
                "Resource": "arn:aws:s3:::BUCKET/*"
            }
        ]
    }

Replace C<BUCKET> with the bucket name that is used by your RT instance.

Finally, set C<$ExternalStorageDirectLink> to 1 in your
F<RT_SiteConfig.pm> file:

    Set($ExternalStorageDirectLink, 1);

=head1 TROUBLESHOOTING

=head2 Issues Connecting to the Amazon Bucket

Here are some things to check if you receive errors connecting to Amazon S3.

=over

=item *

Double check all of the configuration parameters, including the bucket name. Remember to restart
Apache after changing values for RT to load new settings.

=item *

If you manually created a bucket, make sure it is in your default region. Set the Region
option for alternate regions.

=item *

Check the permissions on the bucket and make sure they are sufficient for the user RT is
connecting as to upload and access files. If you are using the direct link option, you will
need to open permissions further for users to access the attachment via the direct link.

=back

=cut

RT::Base->_ImportOverlays();

1;