View | Details | Raw Unified | Return to bug 20638
Collapse All | Expand All

(-)a/Koha/ApiKey.pm (-2 / +101 lines)
Lines 19-25 package Koha::ApiKey; Link Here
19
19
20
use Modern::Perl;
20
use Modern::Perl;
21
21
22
use C4::Log         qw( logaction );
22
use Koha::AuthUtils qw(hash_password);
23
use Koha::AuthUtils qw(hash_password);
24
use Koha::Exceptions::ApiKey;
23
use Koha::Exceptions::Object;
25
use Koha::Exceptions::Object;
24
26
25
use List::MoreUtils qw(any);
27
use List::MoreUtils qw(any);
Lines 44-50 Overloaded I<store> method. Link Here
44
=cut
46
=cut
45
47
46
sub store {
48
sub store {
47
    my ($self) = @_;
49
    my ( $self, $params ) = @_;
50
    $params //= {};
51
52
    my $is_new = !$self->in_storage;
48
53
49
    if ( $self->in_storage ) {
54
    if ( $self->in_storage ) {
50
        my %dirty_columns = $self->_result->get_dirty_columns;
55
        my %dirty_columns = $self->_result->get_dirty_columns;
Lines 60-70 sub store { Link Here
60
            {
65
            {
61
                secret    => Koha::AuthUtils::hash_password( $self->{_plain_text_secret} ),
66
                secret    => Koha::AuthUtils::hash_password( $self->{_plain_text_secret} ),
62
                client_id => $self->_generate_unused_uuid('client_id'),
67
                client_id => $self->_generate_unused_uuid('client_id'),
68
                active    => 1,
63
            }
69
            }
64
        );
70
        );
65
    }
71
    }
66
72
67
    return $self->SUPER::store();
73
    my $result = $self->SUPER::store();
74
75
    # Log the action unless explicitly skipped
76
    if ( !$params->{skip_log} && C4::Context->preference('ApiKeyLog') ) {
77
        if ($is_new) {
78
            logaction(
79
                'APIKEYS', 'CREATE', $self->patron_id,
80
                sprintf( "Client ID: %s, Description: %s", $self->client_id, $self->description )
81
            );
82
        }
83
    }
84
85
    return $result;
68
}
86
}
69
87
70
=head3 validate_secret
88
=head3 validate_secret
Lines 103-108 sub plain_text_secret { Link Here
103
    return;
121
    return;
104
}
122
}
105
123
124
=head3 delete
125
126
    $api_key->delete;
127
128
Overloaded delete method to add logging.
129
130
=cut
131
132
sub delete {
133
    my ($self) = @_;
134
135
    my $client_id   = $self->client_id;
136
    my $description = $self->description;
137
    my $patron_id   = $self->patron_id;
138
139
    my $result = $self->SUPER::delete();
140
141
    if ( C4::Context->preference('ApiKeyLog') ) {
142
        logaction(
143
            'APIKEYS', 'DELETE', $patron_id,
144
            sprintf( "Client ID: %s, Description: %s", $client_id, $description )
145
        );
146
    }
147
148
    return $result;
149
}
150
151
=head3 revoke
152
153
    $api_key->revoke;
154
155
Revokes the API key by setting active to 0.
156
Throws an exception if the key is already revoked.
157
158
=cut
159
160
sub revoke {
161
    my ($self) = @_;
162
163
    Koha::Exceptions::ApiKey::AlreadyRevoked->throw
164
        if !$self->active;
165
166
    $self->active(0)->store( { skip_log => 1 } );
167
168
    if ( C4::Context->preference('ApiKeyLog') ) {
169
        logaction(
170
            'APIKEYS', 'REVOKE', $self->patron_id,
171
            sprintf( "Client ID: %s, Description: %s", $self->client_id, $self->description )
172
        );
173
    }
174
175
    return $self;
176
}
177
178
=head3 activate
179
180
    $api_key->activate;
181
182
Activates the API key by setting active to 1.
183
Throws an exception if the key is already active.
184
185
=cut
186
187
sub activate {
188
    my ($self) = @_;
189
190
    Koha::Exceptions::ApiKey::AlreadyActive->throw
191
        if $self->active;
192
193
    $self->active(1)->store( { skip_log => 1 } );
194
195
    if ( C4::Context->preference('ApiKeyLog') ) {
196
        logaction(
197
            'APIKEYS', 'ACTIVATE', $self->patron_id,
198
            sprintf( "Client ID: %s, Description: %s", $self->client_id, $self->description )
199
        );
200
    }
201
202
    return $self;
203
}
204
106
=head2 Internal methods
205
=head2 Internal methods
107
206
108
=cut
207
=cut
(-)a/Koha/Exceptions/ApiKey.pm (+41 lines)
Line 0 Link Here
1
package Koha::Exceptions::ApiKey;
2
3
use Modern::Perl;
4
5
use Koha::Exception;
6
7
use Exception::Class (
8
    'Koha::Exceptions::ApiKey' => {
9
        isa => 'Koha::Exception',
10
    },
11
    'Koha::Exceptions::ApiKey::AlreadyRevoked' => {
12
        isa         => 'Koha::Exceptions::ApiKey',
13
        description => 'API key is already revoked'
14
    },
15
    'Koha::Exceptions::ApiKey::AlreadyActive' => {
16
        isa         => 'Koha::Exceptions::ApiKey',
17
        description => 'API key is already active'
18
    },
19
);
20
21
=head1 NAME
22
23
Koha::Exceptions::ApiKey - Base class for API key exceptions
24
25
=head1 Exceptions
26
27
=head2 Koha::Exceptions::ApiKey
28
29
Generic API key exception.
30
31
=head2 Koha::Exceptions::ApiKey::AlreadyRevoked
32
33
Exception thrown when trying to revoke an already revoked API key.
34
35
=head2 Koha::Exceptions::ApiKey::AlreadyActive
36
37
Exception thrown when trying to activate an already active API key.
38
39
=cut
40
41
1;
(-)a/t/db_dependent/Koha/ApiKey.t (-3 / +161 lines)
Lines 18-29 Link Here
18
use Modern::Perl;
18
use Modern::Perl;
19
19
20
use Test::NoWarnings;
20
use Test::NoWarnings;
21
use Test::More tests => 5;
21
use Test::More tests => 7;
22
use Test::MockModule;
22
use Test::MockModule;
23
use Test::Exception;
23
use Test::Exception;
24
use Test::Warn;
24
use Test::Warn;
25
25
26
use t::lib::TestBuilder;
26
use t::lib::TestBuilder;
27
use t::lib::Mocks;
27
28
28
BEGIN {
29
BEGIN {
29
    use_ok('Koha::ApiKeys');
30
    use_ok('Koha::ApiKeys');
Lines 121-127 subtest 'store() tests' => sub { Link Here
121
122
122
subtest 'validate_secret() tests' => sub {
123
subtest 'validate_secret() tests' => sub {
123
124
124
    plan tests => 2;
125
    plan tests => 12;
125
126
126
    $schema->storage->txn_begin;
127
    $schema->storage->txn_begin;
127
128
Lines 138-143 subtest 'validate_secret() tests' => sub { Link Here
138
    is( $api_key->validate_secret($secret),        1, 'Valid secret returns true' );
139
    is( $api_key->validate_secret($secret),        1, 'Valid secret returns true' );
139
    is( $api_key->validate_secret('Wrong secret'), 0, 'Invalid secret returns false' );
140
    is( $api_key->validate_secret('Wrong secret'), 0, 'Invalid secret returns false' );
140
141
142
    # Test CREATE logging when ApiKeyLog is enabled
143
    t::lib::Mocks::mock_preference( 'ApiKeyLog', 1 );
144
    my $test_patron = $builder->build_object( { class => 'Koha::Patrons' } );
145
    my $logs_before = $schema->resultset('ActionLog')->search( { module => 'APIKEYS', action => 'CREATE' } )->count;
146
    my $logged_key  = Koha::ApiKey->new( { description => 'logged', patron_id => $test_patron->id } )->store;
147
    my $logs_after  = $schema->resultset('ActionLog')->search( { module => 'APIKEYS', action => 'CREATE' } )->count;
148
    is( $logs_after, $logs_before + 1, 'CREATE action logged when ApiKeyLog enabled' );
149
150
    # Verify log entry parameters
151
    my $log_entry = $schema->resultset('ActionLog')->search(
152
        { module   => 'APIKEYS', action => 'CREATE', object => $test_patron->id },
153
        { order_by => { -desc => 'action_id' } }
154
    )->first;
155
    ok( $log_entry, 'CREATE log entry found' );
156
    is( $log_entry->object, $test_patron->id, 'Log object is patron_id' );
157
    like( $log_entry->info, qr/Client ID: .+, Description: logged/, 'Log info contains client_id and description' );
158
159
    # Test no CREATE logging when ApiKeyLog is disabled
160
    t::lib::Mocks::mock_preference( 'ApiKeyLog', 0 );
161
    $logs_before = $schema->resultset('ActionLog')->search( { module => 'APIKEYS', action => 'CREATE' } )->count;
162
    my $unlogged_key = Koha::ApiKey->new( { description => 'unlogged', patron_id => $test_patron->id } )->store;
163
    $logs_after = $schema->resultset('ActionLog')->search( { module => 'APIKEYS', action => 'CREATE' } )->count;
164
    is( $logs_after, $logs_before, 'CREATE action not logged when ApiKeyLog disabled' );
165
166
    # Test DELETE logging when ApiKeyLog is enabled
167
    t::lib::Mocks::mock_preference( 'ApiKeyLog', 1 );
168
    $logs_before = $schema->resultset('ActionLog')->search( { module => 'APIKEYS', action => 'DELETE' } )->count;
169
    my $client_id_to_delete = $logged_key->client_id;
170
    $logged_key->delete;
171
    $logs_after = $schema->resultset('ActionLog')->search( { module => 'APIKEYS', action => 'DELETE' } )->count;
172
    is( $logs_after, $logs_before + 1, 'DELETE action logged when ApiKeyLog enabled' );
173
174
    # Verify DELETE log entry parameters
175
    my $delete_log = $schema->resultset('ActionLog')->search(
176
        { module   => 'APIKEYS', action => 'DELETE', object => $test_patron->id },
177
        { order_by => { -desc => 'action_id' } }
178
    )->first;
179
    ok( $delete_log, 'DELETE log entry found' );
180
    is( $delete_log->object, $test_patron->id, 'DELETE log object is patron_id' );
181
    like(
182
        $delete_log->info, qr/Client ID: $client_id_to_delete, Description: logged/,
183
        'DELETE log info contains client_id and description'
184
    );
185
186
    # Test no DELETE logging when ApiKeyLog is disabled
187
    t::lib::Mocks::mock_preference( 'ApiKeyLog', 0 );
188
    $logs_before = $schema->resultset('ActionLog')->search( { module => 'APIKEYS', action => 'DELETE' } )->count;
189
    $unlogged_key->delete;
190
    $logs_after = $schema->resultset('ActionLog')->search( { module => 'APIKEYS', action => 'DELETE' } )->count;
191
    is( $logs_after, $logs_before, 'DELETE action not logged when ApiKeyLog disabled' );
192
141
    $schema->storage->txn_rollback;
193
    $schema->storage->txn_rollback;
142
};
194
};
143
195
Lines 158-160 subtest 'plain_text_secret() tests' => sub { Link Here
158
210
159
    $schema->storage->txn_rollback;
211
    $schema->storage->txn_rollback;
160
};
212
};
161
- 
213
214
subtest 'revoke() tests' => sub {
215
216
    plan tests => 8;
217
218
    $schema->storage->txn_begin;
219
220
    my $patron  = $builder->build_object( { class => 'Koha::Patrons' } );
221
    my $api_key = Koha::ApiKey->new( { description => 'test', patron_id => $patron->id } )->store;
222
223
    # Test successful revoke
224
    is( $api_key->active, 1, 'API key starts as active' );
225
    $api_key->revoke;
226
    is( $api_key->active, 0, 'API key is revoked' );
227
228
    # Test exception when already revoked
229
    throws_ok { $api_key->revoke } 'Koha::Exceptions::ApiKey::AlreadyRevoked',
230
        'Exception thrown when trying to revoke already revoked key';
231
232
    # Test logging when ApiKeyLog is enabled
233
    t::lib::Mocks::mock_preference( 'ApiKeyLog', 1 );
234
    my $api_key2 = Koha::ApiKey->new( { description => 'test2', patron_id => $patron->id } )->store;
235
236
    my $logs_before = $schema->resultset('ActionLog')->search( { module => 'APIKEYS', action => 'REVOKE' } )->count;
237
    my $client_id_to_revoke = $api_key2->client_id;
238
    $api_key2->revoke;
239
    my $logs_after = $schema->resultset('ActionLog')->search( { module => 'APIKEYS', action => 'REVOKE' } )->count;
240
    is( $logs_after, $logs_before + 1, 'REVOKE action logged when ApiKeyLog enabled' );
241
242
    # Verify REVOKE log entry parameters
243
    my $revoke_log = $schema->resultset('ActionLog')->search(
244
        { module   => 'APIKEYS', action => 'REVOKE', object => $patron->id },
245
        { order_by => { -desc => 'action_id' } }
246
    )->first;
247
    ok( $revoke_log, 'REVOKE log entry found' );
248
    is( $revoke_log->object, $patron->id, 'REVOKE log object is patron_id' );
249
    like(
250
        $revoke_log->info, qr/Client ID: $client_id_to_revoke, Description: test2/,
251
        'REVOKE log info contains client_id and description'
252
    );
253
254
    # Test no logging when ApiKeyLog is disabled
255
    t::lib::Mocks::mock_preference( 'ApiKeyLog', 0 );
256
    my $api_key3 = Koha::ApiKey->new( { description => 'test3', patron_id => $patron->id } )->store;
257
258
    $logs_before = $schema->resultset('ActionLog')->search( { module => 'APIKEYS', action => 'REVOKE' } )->count;
259
    $api_key3->revoke;
260
    $logs_after = $schema->resultset('ActionLog')->search( { module => 'APIKEYS', action => 'REVOKE' } )->count;
261
    is( $logs_after, $logs_before, 'REVOKE action not logged when ApiKeyLog disabled' );
262
263
    $schema->storage->txn_rollback;
264
};
265
266
subtest 'activate() tests' => sub {
267
268
    plan tests => 8;
269
270
    $schema->storage->txn_begin;
271
272
    my $patron  = $builder->build_object( { class => 'Koha::Patrons' } );
273
    my $api_key = Koha::ApiKey->new( { description => 'test', patron_id => $patron->id } )->store;
274
    $api_key->active(0)->store;    # Start as revoked
275
276
    # Test successful activate
277
    is( $api_key->active, 0, 'API key starts as revoked' );
278
    $api_key->activate;
279
    is( $api_key->active, 1, 'API key is activated' );
280
281
    # Test exception when already active
282
    throws_ok { $api_key->activate } 'Koha::Exceptions::ApiKey::AlreadyActive',
283
        'Exception thrown when trying to activate already active key';
284
285
    # Test logging when ApiKeyLog is enabled
286
    t::lib::Mocks::mock_preference( 'ApiKeyLog', 1 );
287
    my $api_key2 = Koha::ApiKey->new( { description => 'test2', patron_id => $patron->id } )->store;
288
    $api_key2->active(0)->store( { skip_log => 1 } );    # Start as revoked
289
290
    my $logs_before = $schema->resultset('ActionLog')->search( { module => 'APIKEYS', action => 'ACTIVATE' } )->count;
291
    my $client_id_to_activate = $api_key2->client_id;
292
    $api_key2->activate;
293
    my $logs_after = $schema->resultset('ActionLog')->search( { module => 'APIKEYS', action => 'ACTIVATE' } )->count;
294
    is( $logs_after, $logs_before + 1, 'ACTIVATE action logged when ApiKeyLog enabled' );
295
296
    # Verify ACTIVATE log entry parameters
297
    my $activate_log = $schema->resultset('ActionLog')->search(
298
        { module   => 'APIKEYS', action => 'ACTIVATE', object => $patron->id },
299
        { order_by => { -desc => 'action_id' } }
300
    )->first;
301
    ok( $activate_log, 'ACTIVATE log entry found' );
302
    is( $activate_log->object, $patron->id, 'ACTIVATE log object is patron_id' );
303
    like(
304
        $activate_log->info, qr/Client ID: $client_id_to_activate, Description: test2/,
305
        'ACTIVATE log info contains client_id and description'
306
    );
307
308
    # Test no logging when ApiKeyLog is disabled
309
    t::lib::Mocks::mock_preference( 'ApiKeyLog', 0 );
310
    my $api_key3 = Koha::ApiKey->new( { description => 'test3', patron_id => $patron->id } )->store;
311
    $api_key3->active(0)->store( { skip_log => 1 } );    # Start as revoked
312
313
    $logs_before = $schema->resultset('ActionLog')->search( { module => 'APIKEYS', action => 'ACTIVATE' } )->count;
314
    $api_key3->activate;
315
    $logs_after = $schema->resultset('ActionLog')->search( { module => 'APIKEYS', action => 'ACTIVATE' } )->count;
316
    is( $logs_after, $logs_before, 'ACTIVATE action not logged when ApiKeyLog disabled' );
317
318
    $schema->storage->txn_rollback;
319
};

Return to bug 20638