The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use strict;
use 5.010;

package FusqlFS::Backend::PgSQL::Roles;
use FusqlFS::Version;
our $VERSION = $FusqlFS::Version::VERSION;
use parent 'FusqlFS::Artifact';

=begin testing SETUP

#!class FusqlFS::Backend::PgSQL::Test

my $new_role = {
    struct => {
        can_login => 1,
        cat_update => 1,
        config => undef,
        conn_limit => 1,
        create_db => 1,
        create_role => 1,
        inherit => 0,
        superuser => 1,
        valid_until => '2010-01-01 00:00:00+02',
    },
    postgres => \"roles/postgres",
    owned => $_tobj->{owned},
};

=end testing
=cut

use DBI qw(:sql_types);
use FusqlFS::Backend::PgSQL::Role::Owned;

sub init
{
    my $self = shift;

    $self->{list_expr} = $self->expr("SELECT rolname FROM pg_catalog.pg_roles");
    $self->{get_expr} = $self->expr("SELECT r.rolcanlogin AS can_login, r.rolcatupdate AS cat_update, r.rolconfig AS config,
            r.rolconnlimit AS conn_limit, r.rolcreatedb AS create_db, r.rolcreaterole AS create_role, r.rolinherit AS inherit,
            r.rolsuper AS superuser, r.rolvaliduntil AS valid_until,
            ARRAY(SELECT b.rolname FROM pg_catalog.pg_roles AS b
                    JOIN pg_catalog.pg_auth_members AS m ON (m.member = b.oid)
                WHERE m.roleid = r.oid) AS contains
        FROM pg_catalog.pg_roles AS r WHERE rolname = ?");

    $self->{create_expr} = 'CREATE ROLE "%s"';
    $self->{rename_expr} = 'ALTER ROLE "%s" RENAME TO "%s"';
    $self->{drop_expr} = 'DROP ROLE "%s"';

    $self->{revoke_expr} = 'REVOKE "%s" FROM "%s"';
    $self->{grant_expr} = 'GRANT "%s" TO "%s"';

    $self->{owned} = FusqlFS::Backend::PgSQL::Role::Owned->new();
}

=begin testing get

is $_tobj->get('unknown'), undef, 'Unknown role not exists';
is_deeply $_tobj->get('postgres'), { struct => {
    can_login => 1,
    cat_update => 1,
    config => undef,
    conn_limit => '-1',
    create_db => 1,
    create_role => 1,
    inherit => 1,
    superuser => 1,
    valid_until => undef,
},
owned => $_tobj->{owned},
}, 'Known role is sane';

=end testing
=cut
sub get
{
    my $self = shift;
    my ($name) = @_;

    my $data = $self->one_row($self->{get_expr}, $name);
    return unless $data;

    my $result = { map { $_ => \"roles/$_" } @{delete($data->{contains})} };

    $result->{struct} = $self->dump($data);
    $result->{owned}  = $self->{owned};
    return $result;
}

=begin testing list

cmp_deeply $_tobj->list(), supersetof('postgres'), 'Roles list is sane';

=end testing
=cut
sub list
{
    my $self = shift;
    return $self->all_col($self->{list_expr})||[];
}

=begin testing rename after store

isnt $_tobj->rename('fusqlfs_test', 'new_fusqlfs_test'), undef, 'Role renamed';
is_deeply $_tobj->get('new_fusqlfs_test'), $new_role, 'Role renamed correctly';
is $_tobj->get('fusqlfs_test'), undef, 'Role is unaccessable under old name';
my $list = $_tobj->list();
ok grep { $_ eq 'new_fusqlfs_test' } @$list;
ok !grep { $_ eq 'fusqlfs_test' } @$list;

=end testing
=cut
sub rename
{
    my $self = shift;
    my ($name, $newname) = @_;
    $self->do($self->{rename_expr}, [$name, $newname]);
}

=begin testing drop after rename

isnt $_tobj->drop('new_fusqlfs_test'), undef, 'Role deleted';
is $_tobj->get('new_fusqlfs_test'), undef, 'Deleted role is absent';
my $list = $_tobj->list();
ok !grep { $_ eq 'new_fusqlfs_test' } @$list;

=end testing
=cut
sub drop
{
    my $self = shift;
    my ($name) = @_;
    $self->do($self->{drop_expr}, [$name]);
}

=begin testing create after get list

isnt $_tobj->create('fusqlfs_test'), undef, 'Role created';
is_deeply $_tobj->get('fusqlfs_test')->{struct}, {
    can_login => 0,
    cat_update => 0,
    config => undef,
    conn_limit => '-1',
    create_db => 0,
    create_role => 0,
    inherit => 1,
    superuser => 0,
    valid_until => undef,
}, 'New role is sane';

my $list = $_tobj->list();
ok grep { $_ eq 'fusqlfs_test' } @$list;

=end testing
=cut
sub create
{
    my $self = shift;
    my ($name) = @_;
    $self->do($self->{create_expr}, [$name]);
}

=begin testing store after create

isnt $_tobj->store('fusqlfs_test', $new_role), undef, 'Role saved';
is_deeply $_tobj->get('fusqlfs_test'), $new_role, 'Role saved correctly';

=end testing
=cut
sub store
{
    my $self = shift;
    my ($name, $data) = @_;
    my $struct = $self->validate($data, {
        struct => {
            -superuser   => '',
            -create_db   => '',
            -create_role => '',
            -inherit     => '',
            -can_login   => '',
            -conn_limit  => qr/^\d+$/,
            -valid_until => '',
            -password    => '',
        },
    }, sub{
        $_->{contains} = [ grep ref $data->{$_} eq 'SCALAR', keys %{$_[0]} ];
        return 1;
    }) or return;

    my $olddata = $self->one_row($self->{get_expr}, $name);
    my ($grant, $revoke) = $self->adiff($olddata->{contains}, $struct->{contains});

    $self->do($self->{revoke_expr}, [$name, $_]) foreach @$revoke;
    $self->do($self->{grant_expr},  [$name, $_]) foreach @$grant;

    $data = $self->load($data->{struct})||{};

    my $sth = $self->build("ALTER ROLE \"$name\" ", sub{
            my ($a, $b) = @_;
            if (ref $b)
            {
                return unless $data->{$a};
                return "$b->[0] ? ", $data->{$a}, $b->[1];
            }
            else
            {
                return unless exists $data->{$a};
                return ($data->{$a}? '': 'NO') . "$b ";
            }
    }, superuser   => 'SUPERUSER' ,
       create_db   => 'CREATEDB'  ,
       create_role => 'CREATEROLE',
       inherit     => 'INHERIT'   ,
       can_login   => 'LOGIN'     ,
       conn_limit  => ['CONNECTION LIMIT', SQL_INTEGER],
       valid_until => ['VALID UNTIL', SQL_TIMESTAMP]   ,
       password    => ['PASSWORD', SQL_VARCHAR]        );

    $sth->execute();
}

1;