package FS::access_user;
+use base qw( FS::m2m_Common FS::option_Common );
use strict;
-use vars qw( @ISA $htpasswd_file );
+use vars qw( $DEBUG $me );
use FS::UID;
+use FS::Auth;
use FS::Conf;
use FS::Record qw( qsearch qsearchs dbh );
-use FS::m2m_Common;
-use FS::option_Common;
-use FS::access_usergroup;
use FS::agent;
+use FS::cust_main;
+use FS::sales;
-@ISA = qw( FS::m2m_Common FS::option_Common FS::Record );
-#@ISA = qw( FS::m2m_Common FS::option_Common );
-
-#kludge htpasswd for now (i hope this bootstraps okay)
-FS::UID->install_callback( sub {
- my $conf = new FS::Conf;
- $htpasswd_file = $conf->base_dir. '/htpasswd';
-} );
+$DEBUG = 0;
+$me = '[FS::access_user]';
=head1 NAME
=head1 DESCRIPTION
-An FS::access_user object represents an internal access user. FS::access_user inherits from
-FS::Record. The following fields are currently supported:
+An FS::access_user object represents an internal access user. FS::access_user
+inherits from FS::Record. The following fields are currently supported:
=over 4
-=item usernum - primary key
+=item usernum
+
+primary key
+
+=item username
+
+=item _password
+
+=item _password_encoding
+
+Empty or bcrypt
+
+=item last
+
+Last name
+
+=item first
-=item username -
+First name
-=item _password -
+=item user_custnum
-=item last -
+Master customer for this employee (for commissions)
-=item first -
+=item report_salesnum
-=item disabled - empty or 'Y'
+Default sales person for this employee (for reports)
+
+=item disabled
+
+Empty or 'Y'
=back
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- $error = $self->htpasswd_kludge();
if ( $error ) {
$dbh->rollback or die $dbh->errstr if $oldAutoCommit;
return $error;
if ( $error ) {
$dbh->rollback or die $dbh->errstr if $oldAutoCommit;
-
- #make sure it isn't a dup username? or you could nuke people's passwords
- #blah. really just should do our own login w/cookies
- #and auth out of the db in the first place
- #my $hterror = $self->htpasswd_kludge('-D');
- #$error .= " - additionally received error cleaning up htpasswd file: $hterror"
return $error;
-
} else {
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
}
-sub htpasswd_kludge {
- my $self = shift;
-
- #awful kludge to skip setting htpasswd for fs_* users
- return '' if $self->username =~ /^fs_/;
-
- unshift @_, '-c' unless -e $htpasswd_file;
- if (
- system('htpasswd', '-b', @_,
- $htpasswd_file,
- $self->username,
- $self->_password,
- ) == 0
- )
- {
- return '';
- } else {
- return 'htpasswd exited unsucessfully';
- }
-}
-
=item delete
Delete this record from the database.
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- my $error =
- $self->SUPER::delete(@_)
- || $self->htpasswd_kludge('-D')
- ;
+ my $error = $self->SUPER::delete(@_);
if ( $error ) {
$dbh->rollback or die $dbh->errstr if $oldAutoCommit;
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- if ( $new->_password ne $old->_password ) {
- my $error = $new->htpasswd_kludge();
- if ( $error ) {
- $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
- return $error;
- }
- }
+ return "Must change password when enabling this account"
+ if $old->disabled && !$new->disabled
+ && ( $new->_password =~ /changeme/i
+ || $new->_password eq 'notyet'
+ );
my $error = $new->SUPER::replace($old, @_);
my $error =
$self->ut_numbern('usernum')
|| $self->ut_alpha_lower('username')
- || $self->ut_text('_password')
- || $self->ut_text('last')
- || $self->ut_text('first')
+ || $self->ut_textn('_password')
+ || $self->ut_textn('last')
+ || $self->ut_textn('first')
+ || $self->ut_foreign_keyn('user_custnum', 'cust_main', 'custnum')
+ || $self->ut_foreign_keyn('report_salesnum', 'sales', 'salesnum')
|| $self->ut_enum('disabled', [ '', 'Y' ] )
;
return $error if $error;
sub name {
my $self = shift;
- $self->get('last'). ', '. $self->first;
+ return $self->username
+ if $self->get('last') eq 'Lastname' && $self->first eq 'Firstname'
+ or $self->get('last') eq '' && $self->first eq '';
+ return $self->get('last'). ', '. $self->first;
}
-=item access_usergroup
+=item user_cust_main
+
+Returns the FS::cust_main object (see L<FS::cust_main>), if any, for this
+user.
=cut
-sub access_usergroup {
+sub user_cust_main {
my $self = shift;
- qsearch( 'access_usergroup', { 'usernum' => $self->usernum } );
+ qsearchs( 'cust_main', { 'custnum' => $self->user_custnum } );
}
-#=item access_groups
-#
-#=cut
-#
-#sub access_groups {
-#
-#}
-#
-#=item access_groupnames
-#
-#=cut
-#
-#sub access_groupnames {
-#
-#}
+=item report_sales
+
+Returns the FS::sales object (see L<FS::sales>), if any, for this
+user.
+
+=cut
+
+sub report_sales {
+ my $self = shift;
+ qsearchs( 'sales', { 'salesnum' => $self->report_salesnum } );
+}
+
+=item access_usergroup
+
+Returns links to the the groups this user is a part of, as FS::access_usergroup
+objects (see L<FS::access_usergroup>).
=item agentnums
Optional table name in which agentnum is being checked. Sometimes required to
resolve 'column reference "agentnum" is ambiguous' errors.
+=item viewall_right
+
+All agents will be viewable if the current user has the provided access right.
+Defaults to 'View customers of all agents'.
+
=back
=cut
my $agentnum = $opt{'table'} ? $opt{'table'}.'.agentnum' : 'agentnum';
- my @agentnums = map { "$agentnum = $_" } $self->agentnums;
+ my @or = ();
- push @agentnums, "$agentnum IS NULL"
+ my $viewall_right = $opt{'viewall_right'} || 'View customers of all agents';
+ if ( $self->access_right($viewall_right) ) {
+ push @or, "$agentnum IS NOT NULL";
+ } else {
+ my @agentnums = $self->agentnums;
+ push @or, "$agentnum IN (". join(',', @agentnums). ')'
+ if @agentnums;
+ }
+
+ push @or, "$agentnum IS NULL"
if $opt{'null'}
|| ( $opt{'null_right'} && $self->access_right($opt{'null_right'}) );
- return ' 1 = 0 ' unless scalar(@agentnums);
- '( '. join( ' OR ', @agentnums ). ' )';
+ return ' 1 = 0 ' unless scalar(@or);
+ '( '. join( ' OR ', @or ). ' )';
+
}
=item agentnum
Returns true if the user can view the specified agent.
+Also accepts optional hashref cache, to avoid redundant database calls.
+
=cut
sub agentnum {
- my( $self, $agentnum ) = @_;
+ my( $self, $agentnum, $cache ) = @_;
+ $cache ||= {};
+ return $cache->{$self->usernum}->{$agentnum}
+ if $cache->{$self->usernum}->{$agentnum};
my $sth = dbh->prepare(
"SELECT COUNT(*) FROM access_usergroup
JOIN access_groupagent USING ( groupnum )
WHERE usernum = ? AND agentnum = ?"
) or die dbh->errstr;
$sth->execute($self->usernum, $agentnum) or die $sth->errstr;
- $sth->fetchrow_arrayref->[0];
+ $cache->{$self->usernum}->{$agentnum} = $sth->fetchrow_arrayref->[0];
+ $sth->finish;
+ return $cache->{$self->usernum}->{$agentnum};
}
-=item agents
+=item agents [ HASHREF | OPTION => VALUE ... ]
Returns the list of agents this user can view (via group membership), as
-FS::agent objects.
+FS::agent objects. Accepts the same options as the agentnums_sql method.
=cut
qsearch({
'table' => 'agent',
'hashref' => { disabled=>'' },
- 'extra_sql' => ' AND '. $self->agentnums_sql,
+ 'extra_sql' => ' AND '. $self->agentnums_sql(@_),
+ 'order_by' => 'ORDER BY agent',
});
}
-=item access_right
+=item access_users [ HASHREF | OPTION => VALUE ... ]
+
+Returns an array of FS::access_user objects, one for each non-disabled
+access_user in the system that shares an agent (via group membership) with
+the invoking object. Regardless of options and agents, will always at
+least return the invoking user and any users who have viewall_right.
+
+Accepts the following options:
+
+=over 4
+
+=item table
+
+Only return users who appear in the usernum field of this table
-Given a right name, returns true if this user has this right (currently via
-group membership, eventually also via user overrides).
+=item disabled
+
+Include disabled users if true (defaults to false)
+
+=item viewall_right
+
+All users will be returned if the current user has the provided
+access right, regardless of agents (other filters still apply.)
+Defaults to 'View customers of all agents'
+
+=cut
+
+#Leaving undocumented until such time as this functionality is actually used
+#
+#=item null
+#
+#Users with no agents will be returned.
+#
+#=item null_right
+#
+#Users with no agents will be returned if the current user has the provided
+#access right.
+
+sub access_users {
+ my $self = shift;
+ my %opt = ref($_[0]) ? %{$_[0]} : @_;
+ my $table = $opt{'table'};
+ my $search = { 'table' => 'access_user' };
+ $search->{'hashref'} = $opt{'disabled'} ? {} : { 'disabled' => '' };
+ $search->{'addl_from'} = "INNER JOIN $table ON (access_user.usernum = $table.usernum)"
+ if $table;
+ my @access_users = qsearch($search);
+ my $viewall_right = $opt{'viewall_right'} || 'View customers of all agents';
+ return @access_users if $self->access_right($viewall_right);
+ #filter for users with agents $self can view
+ my @out;
+ my $agentnum_cache = {};
+ACCESS_USER:
+ foreach my $access_user (@access_users) {
+ # you can always view yourself, regardless of agents,
+ # and you can always view someone who can view you,
+ # since they might have affected your customers
+ if ( ($self->usernum eq $access_user->usernum)
+ || $access_user->access_right($viewall_right)
+ ) {
+ push(@out,$access_user);
+ next;
+ }
+ # if user has no agents, you need null or null_right to view
+ my @agents = $access_user->agents('viewall_right'=>'NONE'); #handled viewall_right above
+ if (!@agents) {
+ if ( $opt{'null'} ||
+ ( $opt{'null_right'} && $self->access_right($opt{'null_right'}) )
+ ) {
+ push(@out,$access_user);
+ }
+ next;
+ }
+ # otherwise, you need an agent in common
+ foreach my $agent (@agents) {
+ if ($self->agentnum($agent->agentnum,$agentnum_cache)) {
+ push(@out,$access_user);
+ next ACCESS_USER;
+ }
+ }
+ }
+ return @out;
+}
+
+=item access_users_hashref [ HASHREF | OPTION => VALUE ... ]
+
+Accepts same options as L</access_users>. Returns a hashref of
+users, with keys of usernum and values of username.
+
+=cut
+
+sub access_users_hashref {
+ my $self = shift;
+ my %access_users = map { $_->usernum => $_->username }
+ $self->access_users(@_);
+ return \%access_users;
+}
+
+=item access_right RIGHTNAME | LISTREF
+
+Given a right name or a list reference of right names, returns true if this
+user has this right, or, for a list, one of the rights (currently via group
+membership, eventually also via user overrides).
=cut
sub access_right {
my( $self, $rightname ) = @_;
+ $rightname = [ $rightname ] unless ref($rightname);
+
+ warn "$me access_right called on ". join(', ', @$rightname). "\n"
+ if $DEBUG;
+
#some caching of ACL requests for low-hanging fruit perf improvement
#since we get a new $CurrentUser object each page view there shouldn't be any
#issues with stickiness
if ( $self->{_ACLcache} ) {
- return $self->{_ACLcache}{$rightname}
- if exists($self->{_ACLcache}{$rightname});
+
+ unless ( grep !exists($self->{_ACLcache}{$_}), @$rightname ) {
+ warn "$me ACL cache hit for ". join(', ', @$rightname). "\n"
+ if $DEBUG;
+ return scalar( grep $self->{_ACLcache}{$_}, @$rightname );
+ }
+
+ warn "$me ACL cache miss for ". join(', ', @$rightname). "\n"
+ if $DEBUG;
+
} else {
+
+ warn "initializing ACL cache\n"
+ if $DEBUG;
$self->{_ACLcache} = {};
+
}
+ my $has_right = ' rightname IN ('. join(',', map '?', @$rightname ). ') ';
+
my $sth = dbh->prepare("
SELECT groupnum FROM access_usergroup
LEFT JOIN access_group USING ( groupnum )
ON ( access_group.groupnum = access_right.rightobjnum )
WHERE usernum = ?
AND righttype = 'FS::access_group'
- AND rightname = ?
+ AND $has_right
LIMIT 1
") or die dbh->errstr;
- $sth->execute($self->usernum, $rightname) or die $sth->errstr;
+ $sth->execute($self->usernum, @$rightname) or die $sth->errstr;
my $row = $sth->fetchrow_arrayref;
- #$row ? $row->[0] : '';
- $self->{_ACLcache}{$rightname} = ( $row ? $row->[0] : '' );
+ my $return = $row ? $row->[0] : '';
+
+ #just caching the single-rightname hits should be enough of a win for now
+ if ( scalar(@$rightname) == 1 ) {
+ $self->{_ACLcache}{${$rightname}[0]} = $return;
+ }
+
+ $return;
+
+}
+
+=item default_customer_view
+
+Returns the default customer view for this user, from the
+"default_customer_view" user preference, the "cust_main-default_view" config,
+or the hardcoded default, "basics" (formerly "jumbo" prior to 3.0).
+
+=cut
+
+sub default_customer_view {
+ my $self = shift;
+
+ $self->option('default_customer_view')
+ || FS::Conf->new->config('cust_main-default_view')
+ || 'basics'; #s/jumbo/basics/ starting with 3.0
+
+}
+
+=item spreadsheet_format [ OVERRIDE ]
+
+Returns a hashref of this user's Excel spreadsheet download settings:
+'extension' (xls or xlsx), 'class' (Spreadsheet::WriteExcel or
+Excel::Writer::XLSX), and 'mime_type'. If OVERRIDE is 'XLS' or 'XLSX',
+use that instead of the user's setting.
+
+=cut
+
+# is there a better place to put this?
+my %formats = (
+ XLS => {
+ extension => '.xls',
+ class => 'Spreadsheet::WriteExcel',
+ mime_type => 'application/vnd.ms-excel',
+ },
+ XLSX => {
+ extension => '.xlsx',
+ class => 'Excel::Writer::XLSX',
+ mime_type => # it's on wikipedia, it must be true
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ }
+);
+
+sub spreadsheet_format {
+ my $self = shift;
+ my $override = shift;
+
+ my $f = $override
+ || $self->option('spreadsheet_format')
+ || FS::Conf->new->config('spreadsheet_format')
+ || 'XLS';
+
+ $formats{$f};
+}
+
+=item is_system_user
+
+Returns true if this user has the name of a known system account. These
+users cannot log into the web interface and can't have passwords set.
+
+=cut
+
+sub is_system_user {
+ my $self = shift;
+ return grep { $_ eq $self->username } ( qw(
+ fs_queue
+ fs_daily
+ fs_selfservice
+ fs_signup
+ fs_bootstrap
+ fs_selfserv
+ fs_api
+ ) );
+}
+
+=item change_password NEW_PASSWORD
+
+=cut
+
+sub change_password {
+ #my( $self, $password ) = @_;
+ #FS::Auth->auth_class->change_password( $self, $password );
+ FS::Auth->auth_class->change_password( @_ );
+}
+
+=item change_password_fields NEW_PASSWORD
+
+=cut
+sub change_password_fields {
+ #my( $self, $password ) = @_;
+ #FS::Auth->auth_class->change_password_fields( $self, $password );
+ FS::Auth->auth_class->change_password_fields( @_ );
}
=back