198 lines
5.6 KiB
Perl
198 lines
5.6 KiB
Perl
|
|
#!/usr/bin/perl
|
||
|
|
#
|
||
|
|
# DW::API::Key
|
||
|
|
#
|
||
|
|
# Defines API Key objects and provides helper functions for checking them
|
||
|
|
# and the permissions they have, for use with DW::Controller::API::REST endpoints.
|
||
|
|
#
|
||
|
|
# TODO: Many of the helper functions are stubs, to be filled out when we implement API key scoping
|
||
|
|
# Authors:
|
||
|
|
# Ruth Hatch <ruth.s.hatch@gmail.com>
|
||
|
|
#
|
||
|
|
# Copyright (c) 2017 by Dreamwidth Studios, LLC.
|
||
|
|
#
|
||
|
|
# This program is free software; you may redistribute it and/or modify it under
|
||
|
|
# the same terms as Perl itself. For a copy of the license, please reference
|
||
|
|
# 'perldoc perlartistic' or 'perldoc perlgpl'.
|
||
|
|
#
|
||
|
|
|
||
|
|
package DW::API::Key;
|
||
|
|
|
||
|
|
use strict;
|
||
|
|
use warnings;
|
||
|
|
use Carp;
|
||
|
|
|
||
|
|
use LJ::Utils qw(rand_chars);
|
||
|
|
|
||
|
|
# Usage: new_for_user ( user )
|
||
|
|
# Creates a new API key for a given user, saves it to DB,
|
||
|
|
# and returns the new key object.
|
||
|
|
sub new_for_user {
|
||
|
|
my ( $self, $u ) = @_;
|
||
|
|
my $user = LJ::want_user($u)
|
||
|
|
or croak "need a user!\n";
|
||
|
|
|
||
|
|
my $id = LJ::alloc_user_counter( $user, 'B' )
|
||
|
|
or croak 'Unable to allocate user counter for API key.';
|
||
|
|
|
||
|
|
my $key = LJ::rand_chars(32);
|
||
|
|
my $dbw = LJ::get_db_writer() or croak "Failed to get database";
|
||
|
|
$dbw->do(
|
||
|
|
q{INSERT INTO api_key (userid, keyid, hash, state)
|
||
|
|
VALUES (?, ?, ?, 'A')},
|
||
|
|
undef, $user->id, $id, $key
|
||
|
|
);
|
||
|
|
|
||
|
|
if ( $dbw->err ) {
|
||
|
|
carp "Failed to insert key row: " . $dbw->errstr . ".";
|
||
|
|
return undef;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $self->_create( $user, $id, $key );
|
||
|
|
}
|
||
|
|
|
||
|
|
# Usage: lookup ( user, key )
|
||
|
|
# Looks for a given key for a user. Returns the key object
|
||
|
|
# if it's valid, or undef otherwise.
|
||
|
|
sub get_key {
|
||
|
|
my ( $class, $hash ) = @_;
|
||
|
|
return undef unless $hash;
|
||
|
|
my $memkey = "api_key:$hash";
|
||
|
|
|
||
|
|
my $keydata = LJ::MemCache::get($memkey);
|
||
|
|
unless ( defined $keydata ) {
|
||
|
|
my $dbh = LJ::get_db_writer() or croak "Failed to get database";
|
||
|
|
$keydata = $dbh->selectrow_hashref(
|
||
|
|
"SELECT keyid, userid, hash FROM api_key WHERE hash = ? AND state = 'A'",
|
||
|
|
undef, $hash );
|
||
|
|
carp $dbh->errstr if $dbh->err;
|
||
|
|
LJ::MemCache::set( $memkey, $keydata, 60 * 60 * 24 ); # cache for one day
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($keydata) {
|
||
|
|
my $user = LJ::want_user( $keydata->{userid} );
|
||
|
|
return $class->_create( $user, $keydata->{keyid}, $keydata->{hash} );
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
return undef;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Usage: get_keys_for_user ( user )
|
||
|
|
# Looks up all API keys for a given user. Returns an arrayef of key objects,
|
||
|
|
# or undef if the user has no API keys yet.
|
||
|
|
sub get_keys_for_user {
|
||
|
|
my ( $self, $u ) = @_;
|
||
|
|
my $user = LJ::want_user($u)
|
||
|
|
or croak "need a user!\n";
|
||
|
|
my @keylist;
|
||
|
|
|
||
|
|
my $dbh = LJ::get_db_writer() or croak "Failed to get database";
|
||
|
|
my $keys = $dbh->selectall_hashref(
|
||
|
|
q{SELECT keyid, hash FROM api_key WHERE userid = ? AND state = 'A'},
|
||
|
|
'keyid', undef, $user->{userid} );
|
||
|
|
carp $dbh->errstr if $dbh->err;
|
||
|
|
return undef unless $keys;
|
||
|
|
|
||
|
|
for my $key ( sort ( keys %$keys ) ) {
|
||
|
|
my $new = $self->_create( $user, $keys->{$key}->{keyid}, $keys->{$key}->{hash} );
|
||
|
|
push @keylist, $new;
|
||
|
|
}
|
||
|
|
|
||
|
|
# sort oldest to newest (predictable ordering for management page)
|
||
|
|
return [ sort { $a->{keyid} <=> $b->{keyid} } @keylist ];
|
||
|
|
}
|
||
|
|
|
||
|
|
# Usage: create ( user, key )
|
||
|
|
# Creates and returns a new key object given a user and key hash.
|
||
|
|
# Don't call this directly, as it neither verifies nor saves keys.
|
||
|
|
# new_for_user() or get_key() is probably what you want instead.
|
||
|
|
sub _create {
|
||
|
|
my ( $class, $user, $keyid, $keyhash ) = @_;
|
||
|
|
|
||
|
|
my %key = (
|
||
|
|
user => $user,
|
||
|
|
keyid => $keyid,
|
||
|
|
keyhash => $keyhash
|
||
|
|
);
|
||
|
|
|
||
|
|
bless \%key, $class;
|
||
|
|
return \%key;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Usage: $key->can_read( resource )
|
||
|
|
# Checks if a key has been given read permissions
|
||
|
|
# for a given resource by the user it belongs to.
|
||
|
|
|
||
|
|
sub can_read {
|
||
|
|
my ( $self, $resource ) = @_;
|
||
|
|
|
||
|
|
#TODO: Once key scoping is implemented, actually check this
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Usage: $key->can_write( resource )
|
||
|
|
# Checks if a key has been given write permissions
|
||
|
|
# for a given resource by the user it belongs to.
|
||
|
|
|
||
|
|
sub can_write {
|
||
|
|
my ( $self, $resource ) = @_;
|
||
|
|
|
||
|
|
#TODO: Once key scoping is implemented, actually check this
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Usage: $key->delete ($user)
|
||
|
|
# Marks a key as deleted in the DB. A user is required to guarantee
|
||
|
|
# that the key is being deleted by someone with the permission to do so.
|
||
|
|
sub delete {
|
||
|
|
my ( $self, $u ) = @_;
|
||
|
|
my $user = LJ::want_user($u)
|
||
|
|
or croak "need a user!\n";
|
||
|
|
|
||
|
|
$self->valid_for_user($user) or croak "key doesn't belong to user";
|
||
|
|
my $memkey = "api_key:" . $self->{keyhash};
|
||
|
|
|
||
|
|
my $dbw = LJ::get_db_writer() or croak "Failed to get database";
|
||
|
|
$dbw->do( q{UPDATE api_key SET state = 'D' WHERE state = 'A' AND hash = ?},
|
||
|
|
undef, $self->{keyhash} );
|
||
|
|
|
||
|
|
LJ::MemCache::delete($memkey);
|
||
|
|
return 1 unless $dbw->err;
|
||
|
|
|
||
|
|
carp $dbw->errstr if $dbw->err;
|
||
|
|
return undef;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub valid_for_user {
|
||
|
|
my ( $self, $u ) = @_;
|
||
|
|
return $self->{user}->equals($u);
|
||
|
|
}
|
||
|
|
|
||
|
|
sub hash {
|
||
|
|
|
||
|
|
# Returns the "key" itself, or the hash of it
|
||
|
|
return $_[0]->{keyhash};
|
||
|
|
}
|
||
|
|
|
||
|
|
# Usage: get_one (user)
|
||
|
|
# Given a user, either return the first found key for them, or
|
||
|
|
# if they have no keys yet, generate one. Intended for use in
|
||
|
|
# situations where we have a logged in user and want to get a working API
|
||
|
|
# key for them, without forcing them to jump through the menu hoops themselves.
|
||
|
|
sub get_one {
|
||
|
|
my ( $self, $u ) = @_;
|
||
|
|
my $apikeys = $self->get_keys_for_user($u);
|
||
|
|
my $key;
|
||
|
|
|
||
|
|
if ( defined( $apikeys->[0] ) ) {
|
||
|
|
$key = $apikeys->[0];
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$key = $self->new_for_user($u);
|
||
|
|
}
|
||
|
|
return $key;
|
||
|
|
}
|
||
|
|
|
||
|
|
1;
|