# Wallet::Server -- Wallet system server implementation.
# $Id$
#
# Written by Russ Allbery <rra@stanford.edu>
# Copyright 2007 Board of Trustees, Leland Stanford Jr. University
#
# See README for licensing terms.

##############################################################################
# Modules and declarations
##############################################################################

package Wallet::Server;
require 5.006;

use strict;
use vars qw(%MAPPING $VERSION);

use Wallet::ACL;
use Wallet::Object::Keytab;

# This version should be increased on any code change to this module.  Always
# use two digits for the minor version with a leading zero if necessary so
# that it will sort properly.
$VERSION = '0.01';

# This is a mapping of object types to class names, used to determine which
# object implementation should be instantiated for a given object type.
# Currently, there's no dynamic way to recognize new object types, so if you
# extend the wallet system to add new object types, you need to modify this
# list.
%MAPPING = (keytab => 'Wallet::Object::Keytab');

##############################################################################
# Utility methods
##############################################################################

# Create a new wallet server object.  A new server should be created for each
# user who is making changes to the wallet.  Takes the principal and host who
# are sending wallet requests.  Opens a connection to the database that will
# be used for all of the wallet metadata based on the wallet configuration
# information.  We also instantiate the administrative ACL, which we'll use
# for various things.  Throw an exception if anything goes wrong.
sub new {
    my ($class, $user, $host) = @_;
    my $acl = Wallet::ACL->new ('ADMIN');
    unless ($DB_DRIVER and ($DB_INFO or $DB_NAME)) {
        die "database connection information not configured\n";
    }
    my $dsn = "DBI:$DB_DRIVER:";
    if ($DB_INFO) {
        $dsn .= $DB_INFO;
    } else {
        $dsn .= "database=$DB_NAME";
        $dsn .= ";host=$DB_HOST" if $DB_HOST;
        $dsn .= ";port=$DB_PORT" if $DB_PORT;
    }
    my $dbh = DBI->connect ($dsn, $DB_USER, $DB_PASSWORD);
    if (not defined $dbh) {
        die "cannot connect to database: $DBI::errstr\n";
    }
    $dbh->{AutoCommit} = 0;
    $dbh->{RaiseError} = 1;
    $dbh->{PrintError} = 0;
    my $self = {
        dbh   => $dbh,
        user  => $user,
        host  => $host,
        admin => $acl,
    };
    bless ($self, $class);
    return $self;
}

# Returns the error from the previous failed operation.
sub error {
    my ($self) = @_;
    return $self->{error};
}

##############################################################################
# Object methods
##############################################################################

# Create a new object and returns that object.  On error, returns undef and
# sets the internal error.
#
# For the time being, we hard-code an ACL named ADMIN to use to authorize
# object creation.  This needs more work later.
sub create {
    my ($self, $name, $type) = @_;
    unless ($MAPPING{$type}) {
        $self->{error} = "unknown object type $type";
        return undef;
    }
    my $class = $MAPPING{$type};
    my $dbh = $self->{dbh};
    my $user = $self->{user};
    my $host = $self->{host};
    unless ($self->{admin}->check ($user)) {
        $self->{error} = "$user not authorized to create ${type}:${name}";
        return undef;
    }
    my $object = eval { $class->create ($name, $type, $dbh, $user, $host) };
    if ($@) {
        $self->{error} = $@;
        return undef;
    } else {
        return $object;
    }
}

# Given the name and type of an object, returns a Perl object representing it
# or returns undef and sets the internal error.
sub retrieve {
    my ($self, $name, $type) = @_;
    unless ($MAPPING{$type}) {
        $self->{error} = "unknown object type $type";
        return undef;
    }
    my $class = $MAPPING{$type};
    my $object = eval { $class->new ($name, $type, $self->{dbh}) };
    if ($@) {
        $self->{error} = $@;
        return undef;
    } else {
        return $object;
    }
}

# Sets the internal error variable to the correct message for permission
# denied on an object.
sub object_error {
    my ($self, $object, $action) = @_;
    my $user = $self->{user};
    my $id = $object->type . ':' . $object->name;
    if ($action !~ /^(create|get|set|show|destroy)\z/) {
        $action = "set $action for";
    }
    $self->{error} = "$self->{user} not authorized to $action $id";
}

# Given an object and an action, checks if the current user has access to
# perform that object.  If so, returns true.  If not, returns undef and sets
# the internal error message.
sub acl_check {
    my ($self, $object, $action) = @_;
    unless ($action =~ /^(get|store|show|destroy|flags)\z/) {
        $self->{error} = "unknown action $action";
        return undef;
    }
    return 1 if $self->{admin}->check ($self->{user});
    my $id = $object->acl ($action);
    if (not defined $id && $action =~ /^(get|store|show)\z/) {
        $id = $object->owner;
    }
    unless (defined $id) {
        $self->object_error ($object, $action);
        return undef;
    }
    my $acl = eval { Wallet::ACL->new ($id) };
    if ($@) {
        $self->{error} = $@;
        return undef;
    }
    my $status = $acl->check ($self->{user});
    if ($status == 1) {
        return 1;
    } elsif (not defined $status) {
        $self->{error} = $acl->error;
        return undef;
    } else {
        $self->object_error ($object, $action);
        return undef;
    }
}

# Retrieves or sets an ACL on an object.
sub acl {
    my ($self, $name, $type, $acl, $id) = @_;
    my $object = $self->retrieve ($name, $type);
    return undef unless defined $object;
    unless ($self->{admin}->check ($self->{user})) {
        $self->object_error ($object, 'ACL');
        return undef;
    }
    if ($id) {
        return $object->acl ($acl, $id, $self->{user}, $self->{host});
    } else {
        return $object->acl ($acl);
    }
}

# Retrieves or sets the expiration of an object.
sub expires {
    my ($self, $name, $type, $expires) = @_;
    my $object = $self->retrieve ($name, $type);
    return undef unless defined $object;
    unless ($self->{admin}->check ($self->{user})) {
        $self->object_error ($object, 'expires');
        return undef;
    }
    if ($expires) {
        return $object->expires ($expires, $self->{user}, $self->{host});
    } else {
        return $object->expires;
    }
}

# Retrieves or sets the owner of an object.
sub owner {
    my ($self, $name, $type, $owner) = @_;
    my $object = $self->retrieve ($name, $type);
    return undef unless defined $object;
    unless ($self->{admin}->check ($self->{user})) {
        $self->object_error ($object, 'owner');
        return undef;
    }
    if ($owner) {
        return $object->owner ($owner, $self->{user}, $self->{host});
    } else {
        return $object->owner;
    }
}

# Retrieve the information associated with an object, or returns undef and
# sets the internal error if the retrieval fails or if the user isn't
# authorized.
sub get {
    my ($self, $name, $type) = @_;
    my $object = $self->retrieve ($name, $type);
    return undef unless defined $object;
    return undef unless $self->acl_check ($object, 'get');
    return $object->get ($self->{user}, $self->{host});
}

# Store new data in an object, or returns undef and sets the internal error if
# the object can't be found or if the user isn't authorized.
sub store {
    my ($self, $name, $type, $data) = @_;
    my $object = $self->retrieve ($name, $type);
    return undef unless defined $object;
    return undef unless $self->acl_check ($object, 'store');
    return $object->store ($data, $self->{user}, $self->{host});
}

# Return a human-readable description of the object's metadata, or returns
# undef and sets the internal error if the object can't be found or if the
# user isn't authorized.
sub show {
    my ($self, $name, $type) = @_;
    my $object = $self->retrieve ($name, $type);
    return undef unless defined $object;
    return undef unless $self->acl_check ($object, 'show');
    return $object->show;
}

# Destroys the object, or returns undef and sets the internal error if the
# object can't be found or if the user isn't authorized.
sub destroy {
    my ($self, $name, $type) = @_;
    my $object = $self->retrieve ($name, $type);
    return undef unless defined $object;
    unless ($self->{admin}->check ($self->{user})) {
        $self->object_error ($object, 'owner');
        return undef;
    }
    return $object->destroy ($self->{user}, $self->{host});
}

##############################################################################
# ACL methods
##############################################################################

# Create a new empty ACL in the database.  Returns the new ACL object on
# success and undef on failure, setting the internal error.
sub acl_create {
    my ($self, $name) = @_;
    unless ($self->{admin}->check ($self->{user})) {
        $self->{error} = "$self->{user} not authorized to create ACL";
        return undef;
    }
    my $dbh = $self->{dbh};
    my $user = $self->{user};
    my $host = $self->{host};
    my $acl = eval { Wallet::ACL->create ($name, $dbh, $user, $host) };
    if ($@) {
        $self->{error} = $@;
        return undef;
    } else {
        return $acl;
    }
}

# Sets the internal error variable to the correct message for permission
# denied on an ACL.
sub acl_error {
    my ($self, $acl, $action) = @_;
    my $user = $self->{user};
    if ($action eq 'add') {
        $action = 'add to';
    } elsif ($action eq 'remove') {
        $action = 'remove from';
    }
    $self->{error} = "$self->{user} not authorized to $action ACL $acl";
}

# Change the human-readable name of an ACL or return undef and set the
# internal error.
sub acl_rename {
    my ($self, $id, $name) = @_;
    unless ($self->{admin}->check ($self->{user})) {
        $self->acl_error ($id, 'rename');
        return undef;
    }
    my $acl = { Wallet::ACL->new ($id, $self->{dbh}) };
    if ($@) {
        $self->{error} = $@;
        return undef;
    }
    unless ($acl->rename ($name)) {
        $self->{error} = $acl->error;
        return undef;
    }
    return 1;
}

# Destroy an ACL, deleting it out of the database.  Returns true on success.
# On failure, returns undef, setting the internal error.
sub acl_destroy {
    my ($self, $id) = @_;
    unless ($self->{admin}->check ($self->{user})) {
        $self->acl_error ($id, 'destroy');
        return undef;
    }
    my $acl = { Wallet::ACL->new ($id, $self->{dbh}) };
    if ($@) {
        $self->{error} = $@;
        return undef;
    }
    unless ($acl->destroy ($self->{user}, $self->{host})) {
        $self->{error} = $acl->error;
        return undef;
    }
    return 1;
}

# Add an ACL entry to an ACL.  Returns true on success.  On failure, returns
# undef, setting the internal error.
sub acl_add {
    my ($self, $id, $scheme, $identifier) = @_;
    unless ($self->{admin}->check ($self->{user})) {
        $self->acl_error ($id, 'add');
        return undef;
    }
    my $acl = { Wallet::ACL->new ($id, $self->{dbh}) };
    if ($@) {
        $self->{error} = $@;
        return undef;
    }
    unless ($acl->add ($scheme, $identifier, $self->{user}, $self->{host})) {
        $self->{error} = $acl->error;
        return undef;
    }
    return 1;
}

# Remove an ACL entry to an ACL.  Returns true on success.  On failure,
# returns undef, setting the internal error.
sub acl_remove {
    my ($self, $id, $scheme, $identifier) = @_;
    unless ($self->{admin}->check ($self->{user})) {
        $self->acl_error ($id, 'remove');
        return undef;
    }
    my $acl = { Wallet::ACL->new ($id, $self->{dbh}) };
    if ($@) {
        $self->{error} = $@;
        return undef;
    }
    my $user = $self->{user};
    my $host = $self->{host};
    unless ($acl->remove ($scheme, $identifier, $user, $host)) {
        $self->{error} = $acl->error;
        return undef;
    }
    return 1;
}

1;
__END__