Bivio::Biz::Util::RealmRole
# Copyright (c) 1999-2012 bivio Software, Inc. All rights reserved.
# $Id$
package Bivio::Biz::Util::RealmRole;
use strict;
use Bivio::Base 'Bivio.ShellUtil';
b_use('IO.Trace');
our($_TRACE);
my($_IDI) = __PACKAGE__->instance_data_index;
Bivio::IO::Trace->register;
my($_CATEGORY_MAP);
b_use('IO.Config')->register(my $_CFG = {
category_map => sub {[]},
});
my($_R) = b_use('Auth.Role');
my($_P) = b_use('Auth.Permission');
my($_PS) = b_use('Auth.PermissionSet');
my($_AR) = b_use('Auth.Realm');
my($_RT) = b_use('Auth.RealmType');
my($_SUPER_USER_QUERY) = {
realm_id => $_RT->GENERAL->as_int,
role => $_R->ADMINISTRATOR,
};
sub CATEGORIES {
# : array_ref
# Returns categories in L<CATEGORY_MAP|"CATEGORY_MAP">
return [keys(%{_category_map(shift)})];
}
sub USAGE {
# : string
# Returns usage.
return <<'EOF';
usage: b-realm-role [options] command [args...]
commands:
audit_feature_categories -- audit enabled categories for a realm
clear_unused_permissions -- clear permission named UNUSED_...
copy_all src dst -- copies all records from src to dst realm
edit role|group operation ... -- changes the permissions for realm/role|group
edit_categories [category_op ...] -- disable or enable permission categories
list [role|group] -- lists permissions for this realm and role|group, or all
list_all [realm_type] -- lists permissions for all realms of realm_type
list_all_categories -- lists all defined permission categories
list_enabled_categories -- list enabled permission categories for this realm
list_roles role|group -- roles for category role group designator
make_super_user -- gives current user super_user privileges
permission_count -- show count by permission used in entire database
roles_for_permissions permission... -- list roles which have permission(s)
set_same old new - copies permission old to new for ALL realms
unmake_super_user -- drops current user's super_user privileges
EOF
}
sub audit_feature_categories {
my($self) = @_;
my($categories) = $self->list_enabled_categories;
foreach my $perm (@{
$_PS->to_array($self->model('RealmRole', {
role => $_R->ANONYMOUS,
})->get('permission_set'))
}) {
my($name) = lc($perm->get_name);
next unless $name =~ /^feature_/;
next if grep($_ eq $name, @$categories);
$self->print('corrupt feature: ', $name, "\n");
}
return;
}
sub clear_unused_permissions {
my($self) = @_;
my($perms) = [grep($_->get_name =~ /^UNUSED_\d+$/, $_P->get_list)];
$self->model('RealmRole')->do_iterate(sub {
my($rr) = @_;
my($set) = $rr->get('permission_set');
my($changed) = 0;
foreach my $p (@$perms) {
next unless $_PS->is_set($set, [$p]);
$changed = 1;
b_info($p);
$set = $_PS->clear($set, [$p]);
}
if ($changed) {
$rr->update({
permission_set => $$set,
});
}
return 1;
}, 'unauth_iterate_start');
return;
}
sub copy_all {
my($self, $src, $dst) = @_;
my($req) = $self->get_request;
($src, $dst) = map(
$self->model('RealmOwner')
->unauth_load_by_id_or_name_or_die($_)->get('realm_id'),
$src, $dst,
);
$self->model('RealmRole')->do_iterate(
sub {
my($m) = @_;
$m->new_other('RealmRole')->unauth_create_or_update({
%{$m->get_shallow_copy},
realm_id => $dst,
});
return 1;
},
unauth_iterate_start => ('realm_id', {realm_id => $src}),
);
return;
}
sub do_super_users {
my($self, $op) = @_;
return $self->model('RealmUser')->do_iterate(
sub {
return $self->req->with_user(
shift->get('user_id'),
sub {
$op->();
return 1;
},
);
},
'unauth_iterate_start',
'user_id',
{%$_SUPER_USER_QUERY},
);
}
sub edit {
sub EDIT {[[qw(role_or_group Line)], [qw(+operations Line)]]}
my($self, $bp) = shift->parameters(\@_);
my($roles) = $_R->calculate_expression($bp->{role_or_group});
my($req) = $self->req;
my($realm) = $req->get('auth_realm');
my($realm_id) = $realm->get('id');
$self->model('RealmRole')->initialize_permissions($realm->get('owner'));
foreach my $role (@$roles) {
my($ps) = _get_permission_set($self, $realm_id, $role, 1);
_trace('current ', $role, ' ', $_PS->to_literal($ps))
if $_TRACE;
foreach my $op (@{$bp->{operations}}) {
$self->usage_error("$op: invalid operation syntax")
unless $op =~ /^([-+])(\w*)$/;
$ps = _edit_one($self, $1, uc($2), $ps, $op, $realm_id);
}
$self->model('RealmRole')->create_or_update({
role => $role,
permission_set => $ps,
});
}
return;
}
sub edit_categories {
my($self, $category_ops) = _edit_categories_args(@_);
return
unless @$category_ops;
my($req) = $self->get_request;
my($rr) = $self->model('RealmRole');
my($o) = $req->get('auth_realm')->get('owner');
foreach my $category_op (@$category_ops) {
my($op, $cat) = $category_op =~ /^(-|\+)(\w+)$/;
$self->usage_error($category_op, ': unknown category operation (missing "+"?)')
unless $op;
foreach my $x (@{
_category_map($self)->{$cat}->{$op}
|| $self->usage_error(
$cat, ': unknown category (case-sensitive)')
}) {
my($method, $roles, $permissions) = @$x;
$rr->$method($o, $roles, $permissions);
}
}
return join(' ', $o->get('name') . ':', @$category_ops) . "\n";
}
sub handle_config {
# (proto, hash) : undef
my(undef, $cfg) = @_;
$_CATEGORY_MAP = undef;
$_CFG = $cfg;
return;
}
sub is_category {
my($self, $value) = @_;
return _category_map($self)->{$value} ? 1 : 0;
}
sub list {
# (self, string) : undef
# Print the permission sets so they can be used as input to this program.
# If I<role_name> is C<undef>, gets all roles.
my($self, $role_name) = @_;
return _list_one(
$self,
$self->req('auth_realm'),
$_R->calculate_expression($role_name),
);
}
sub list_all {
sub LIST_ALL {[[qw(?realm_type Auth.RealmType)]]}
my($self, $bp) = shift->parameters(\@_);
my($sep) = '';
my($roles) = $_R->calculate_expression();
my($res) = '';
$self->model('RealmOwner')->do_iterate(
sub {
my($it) = @_;
my($p) = _list_one($self, $_AR->new($it->clone), $roles);
return 1
unless $$p;
$res .= <<"EOF" . $$p;
$sep#
# @{[$it->get('name')]} - Permissions
#
EOF
$sep = "\n";
return 1;
},
'unauth_iterate_start',
'realm_id',
$bp->{realm_type} ? {realm_type => $bp->{realm_type}} : (),
);
return \$res;
}
sub list_all_categories {
return shift->CATEGORIES;
}
sub list_enabled_categories {
# (self) : array_ref
# Shows permission categories which are enabled for the current realm.
my($self) = @_;
my($req) = $self->get_request;
my($rp) = $self->model('RealmRole')
->get_permission_map($req->get('auth_realm'));
my($cm) = _category_map($self);
return [map({
my($k) = $_;
my($ops) = $cm->{$k}->{'+'};
@$ops == grep({
my($op, $roles, $permissions) = @$_;
@$roles == grep(
((defined($rp->{$_})
&& (($rp->{$_} & $permissions) eq $permissions))
xor ($op eq 'remove_permissions')),
@$roles);
} @$ops) ? $k : ();
} sort(keys(%$cm)))];
}
sub list_roles {
my($self, $role_or_group) = @_;
return [map($_->get_name, @{$_R->calculate_expression($role_or_group)})];
}
sub make_super_user {
# (self) : undef
# Makes current user an super_user (administrator of general realm).
my($self) = @_;
$self->model('RealmUser')->unauth_create_or_update({
user_id => $self->req('auth_user_id'),
%$_SUPER_USER_QUERY,
});
$self->req->set_user($self->req->get('auth_user'));
return;
}
sub new {
# (proto) : Util.RealmRole
# Initializes fields.
my($self) = shift->SUPER::new(@_);
$self->[$_IDI] = {};
return $self;
}
sub permission_count {
my($self) = @_;
my($perms) = [sort({$a->as_int <=> $b->as_int} $_P->get_list)];
my($perm_count) = {
map(($_->as_int => 0), @$perms),
};
$self->model('RealmRole')->do_iterate(sub {
my($rr) = @_;
my($set) = $rr->get('permission_set');
foreach my $p (@$perms) {
$perm_count->{$p->as_int}++
if $_PS->is_set($set, [$p]);
}
return 1;
}, 'unauth_iterate_start');
my($res) = [['count', 'permission']];
foreach my $p (@$perms) {
push(@$res, [$perm_count->{$p->as_int}, $p->get_name]);
}
return $self->new_other('CSV')->to_csv_text($res);
}
sub set_same {
# (self, string, string) : undef
# Sets I<new> permission to same value as I<old> permission. This is used
# to add new permissions to the permission_set of all realms and roles
# in the database. The I<old> permission is a model for the I<new>
# permission. If the I<old> permission is set, the I<new> permission for the
# same realm/role combination. It can be used to adjust existing permissions.
my($self, $old, $new) = @_;
$self->usage('set_same: missing args')
unless defined($new) && defined($old);
my($new_int) = $_P->from_any($new)->as_int;
my($old_int) = $_P->from_any($old)->as_int;
my($rr) = $self->model('RealmRole');
my($it) = $rr->unauth_iterate_start('realm_id, role');
while ($rr->iterate_next_and_load($it)) {
my($s) = $rr->get('permission_set');
vec($s, $new_int, 1) = vec($s, $old_int, 1);
$rr->update({permission_set => $s});
}
$rr->iterate_end($it);
return;
}
sub roles_for_permissions {
sub ROLES_FOR_PERMISSIONS {[[qw(+permission Auth.Permission)]]}
my($self, $bp) = shift->parameters(\@_);
my($ps) = ${$_PS->from_array($bp->{permission})};
my($rp) = $self->model('RealmRole')
->get_permission_map($self->req('auth_realm'));
return [
map(
defined($rp->{$_}) && ($rp->{$_} & $ps) eq $ps ? $_->get_name : (),
@{$_R->calculate_expression()},
),
]
}
sub unmake_super_user {
# (self) : undef
# Drops current user as super_user. See L<make_super_user|"make_super_user">.
my($self) = @_;
my($req) = $self->get_request;
$self->model('RealmUser')->unauth_delete({
user_id => $req->get('auth_user_id')
|| $self->usage_error('user not set'),
%$_SUPER_USER_QUERY,
});
$self->req->set_user($self->req->get('auth_user'));
return;
}
sub _category_map {
return $_CATEGORY_MAP ||= _init_category_map(@_);
}
sub _edit_categories_args {
my($self) = shift;
$self->usage('missing category_ops')
unless @_;
return ($self, [reverse(
sort(
map(
{
my($v) = $_;
ref($v) eq 'ARRAY' ? @$v
: ref($v) eq 'HASH'
? map(
($v->{$_} ? '+' : '-') . $_,
sort(keys(%$v)),
)
: $v;
}
@_,
),
),
)]);
}
sub _edit_one {
my($self, $which, $operand, $ps, $op, $realm_id) = @_;
return $which eq '+' ? $_PS->get_max : $_PS->get_min
unless length($operand);
my($p) = $_P->unsafe_from_any($operand);
b_die($p, ': cannot set TRANSIENT permissions')
if $which eq '+' && $p && $p->get_name =~ /TRANSIENT/;
if ($p && $p->get_name eq $operand) {
vec($ps, $p->as_int, 1) = $which eq '+' ? 1 : 0;
return $ps;
}
my($r) = $_R->unsafe_from_any($operand);
$self->usage($op, ': neither a Role nor Permission')
unless $r && $r->get_name eq $operand;
my($s) = _get_permission_set($self, $realm_id, $r, 0);
_trace($which, $r, ' ', $_PS->to_literal($s))
if $_TRACE;
# Set lengths must match for ~$s to work properly
b_die(
'ASSERTION FAULT: set lengths differ: ',
length($s),
' != ',
length($ps),
) if length($s) != length($ps);
return $which eq '+' ? ($ps | $s) : ($ps & ~$s);
}
sub _get_permission_set {
my($self, $realm_id, $role, $dont_die) = @_;
my($rr) = $self->model('RealmRole');
return $rr->get('permission_set')
if $rr->unauth_load(realm_id => $realm_id, role => $role);
$self->usage($role->as_string, ": not set for realm: ", $realm_id)
unless $dont_die;
return $_PS->get_min;
}
sub _init_category_map {
my($proto) = @_;
my($map) = {};
foreach my $x (@{$_CFG->{category_map}->()}) {
my($cat, @ops) = @$x;
$map->{$cat} = {map({
my($sign) = $_;
$sign => [map(
ref($_) ? _init_category_map_op($proto, $cat, $sign, $_)
: _init_category_map_copy($proto, $cat, $sign, $_, $map),
@ops,
)];
} qw(+ -))};
}
return $map;
}
sub _init_category_map_copy {
my($proto, $cat, $sign, $copy_cat, $map) = @_;
$copy_cat =~ s/^\+//;
my($reverse) = $copy_cat =~ s/^-//;
return map({
my($method, @rest) = @$_;
[
$reverse ? $method eq 'add_permissions'
? 'remove_permissions'
: 'add_permissions'
: $method,
@rest,
];
} @{($map->{$copy_cat} || b_die($copy_cat, ': not listed before ', $cat))
->{$sign}});
}
sub _init_category_map_op {
my($proto, $cat, $sign, $op) = @_;
my($roles, $perms, @rest) = map(ref($_) ? $_ : [$_], @$op);
b_die(
$cat,
': invalid category_map entry; extra params: ',
\@rest,
) if @rest;
return map(
{
my($x) = $_;
$x =~ s/^\+//;
[
($x =~ s/^-// xor $sign eq '-')
? 'remove_permissions' : 'add_permissions',
[map(@{$_R->calculate_expression($_)}, @$roles)],
${$_PS->set($_PS->get_min, $_P->$x())},
];
}
@$perms,
);
}
sub _list_one {
# (self, Auth.Realm, array_ref) : string_ref
# Lists the roles for realm_id.
my($self, $realm, $roles) = @_;
my($fields) = $self->[$_IDI];
$fields->{all_permissions} = [
sort {
$a->get_name cmp $b->get_name
} $_P->get_list
] unless $fields->{all_permissions};
my($rr) = $self->model('RealmRole');
my($res) = "RealmRole();\n";
my($prev_ps, $prev_role);
my($realm_id, $realm_name) = $realm->unsafe_get(qw(id owner_name));
$realm_name = $realm->get('type')->get_name unless $realm_name;
foreach my $role (@$roles) {
unless ($rr->unauth_load(realm_id => $realm_id, role => $role)) {
next;
}
# Always clear the set before adding in values
$res .= "RealmRole(qw(-r $realm_name edit " . $role->get_name;
my($ps) = $rr->get('permission_set');
if ($ps eq $_PS->get_max) {
$res .= ' +';
}
else {
$res .= ' -';
# If the previous role is a subset, delete those bits and
# just add the role to the output.
my($s) = $ps;
if (defined($prev_ps) && ($prev_ps & $ps) eq $prev_ps) {
$res .= "\n +$prev_role";
$s &= ~$prev_ps;
}
foreach my $p (@{$fields->{all_permissions}}) {
$res .= "\n +".$p->get_name
if vec($s, $p->as_int, 1);
}
}
$res .= "\n));\n";
$prev_role = $role->get_name;
$prev_ps = $ps;
}
return \$res;
}
1;