1# BEGIN BPS TAGGED BLOCK {{{ 2# 3# COPYRIGHT: 4# 5# This software is Copyright (c) 1996-2021 Best Practical Solutions, LLC 6# <sales@bestpractical.com> 7# 8# (Except where explicitly superseded by other copyright notices) 9# 10# 11# LICENSE: 12# 13# This work is made available to you under the terms of Version 2 of 14# the GNU General Public License. A copy of that license should have 15# been provided with this software, but in any event can be snarfed 16# from www.gnu.org. 17# 18# This work is distributed in the hope that it will be useful, but 19# WITHOUT ANY WARRANTY; without even the implied warranty of 20# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 21# General Public License for more details. 22# 23# You should have received a copy of the GNU General Public License 24# along with this program; if not, write to the Free Software 25# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 26# 02110-1301 or visit their web page on the internet at 27# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. 28# 29# 30# CONTRIBUTION SUBMISSION POLICY: 31# 32# (The following paragraph is not intended to limit the rights granted 33# to you to modify and distribute this software under the terms of 34# the GNU General Public License and is only of importance to you if 35# you choose to contribute your changes and enhancements to the 36# community by submitting them to Best Practical Solutions, LLC.) 37# 38# By intentionally submitting any modifications, corrections or 39# derivatives to this work, or any other work intended for use with 40# Request Tracker, to Best Practical Solutions, LLC, you confirm that 41# you are the copyright holder for those contributions and you grant 42# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, 43# royalty-free, perpetual, license to use, copy, create derivative 44# works based on those contributions, and sublicense and distribute 45# those contributions and any derivatives thereof. 46# 47# END BPS TAGGED BLOCK }}} 48 49use strict; 50use warnings; 51 52package RT::Catalog; 53use base 'RT::Record'; 54 55use Role::Basic 'with'; 56with "RT::Record::Role::Lifecycle", 57 "RT::Record::Role::Roles" => { 58 -rename => { 59 # We provide ACL'd wraps of these. 60 AddRoleMember => "_AddRoleMember", 61 DeleteRoleMember => "_DeleteRoleMember", 62 RoleGroup => "_RoleGroup", 63 }, 64 }, 65 "RT::Record::Role::Rights"; 66 67require RT::ACE; 68 69=head1 NAME 70 71RT::Catalog - A logical set of assets 72 73=cut 74 75# For the Lifecycle role 76sub LifecycleType { "asset" } 77 78# Setup rights 79__PACKAGE__->AddRight( General => ShowCatalog => 'See catalogs' ); #loc 80__PACKAGE__->AddRight( Admin => AdminCatalog => 'Create, modify, and disable catalogs' ); #loc 81 82__PACKAGE__->AddRight( General => ShowAsset => 'See assets' ); #loc 83__PACKAGE__->AddRight( Staff => CreateAsset => 'Create assets' ); #loc 84__PACKAGE__->AddRight( Staff => ModifyAsset => 'Modify assets' ); #loc 85 86__PACKAGE__->AddRight( General => SeeCustomField => 'View custom field values' ); # loc 87__PACKAGE__->AddRight( Staff => ModifyCustomField => 'Modify custom field values' ); # loc 88__PACKAGE__->AddRight( Staff => SetInitialCustomField => 'Add custom field values only at object creation time'); # loc 89 90RT::ACE->RegisterCacheHandler(sub { 91 my %args = ( 92 Action => "", 93 RightName => "", 94 @_ 95 ); 96 97 return unless $args{Action} =~ /^(Grant|Revoke)$/i 98 and $args{RightName} =~ /^(ShowCatalog|CreateAsset)$/; 99 100 RT::Catalog->CacheNeedsUpdate(1); 101}); 102 103=head1 DESCRIPTION 104 105Catalogs are for assets what queues are for tickets or classes are for 106articles. 107 108It announces the rights for assets, and rights are granted at the catalog or 109global level. Asset custom fields are either applied globally to all Catalogs 110or individually to specific Catalogs. 111 112=over 4 113 114=item id 115 116=item Name 117 118Limited to 255 characters. 119 120=item Description 121 122Limited to 255 characters. 123 124=item Lifecycle 125 126=item Disabled 127 128=item Creator 129 130=item Created 131 132=item LastUpdatedBy 133 134=item LastUpdated 135 136=back 137 138All of these are readable through methods of the same name and mutable through 139methods of the same name with C<Set> prefixed. The last four are automatically 140managed. 141 142=head1 METHODS 143 144=head2 Load ID or NAME 145 146Loads the specified Catalog into the current object. 147 148=cut 149 150sub Load { 151 my $self = shift; 152 my $id = shift; 153 return unless $id; 154 155 if ( $id =~ /\D/ ) { 156 return $self->LoadByCols( Name => $id ); 157 } 158 else { 159 return $self->SUPER::Load($id); 160 } 161} 162 163=head2 Create PARAMHASH 164 165Create takes a hash of values and creates a row in the database. Available keys are: 166 167=over 4 168 169=item Name 170 171=item Description 172 173=item Lifecycle 174 175=item HeldBy, Contact 176 177A single principal ID or array ref of principal IDs to add as members of the 178respective role groups for the new catalog. 179 180User Names and EmailAddresses may also be used, but Groups must be referenced 181by ID. 182 183=item Disabled 184 185=back 186 187Returns a tuple of (status, msg) on failure and (id, msg, non-fatal errors) on 188success, where the third value is an array reference of errors that occurred 189but didn't prevent creation. 190 191=cut 192 193sub Create { 194 my $self = shift; 195 my %args = ( 196 Name => '', 197 Description => '', 198 Lifecycle => 'assets', 199 200 HeldBy => undef, 201 Contact => undef, 202 203 Disabled => 0, 204 205 @_ 206 ); 207 my @non_fatal_errors; 208 209 return (0, $self->loc("Permission Denied")) 210 unless $self->CurrentUserHasRight('AdminCatalog'); 211 212 return (0, $self->loc('Invalid Name (names must be unique and may not be all digits)')) 213 unless $self->ValidateName( $args{'Name'} ); 214 215 $args{'Lifecycle'} ||= 'assets'; 216 217 return (0, $self->loc('[_1] is not a valid lifecycle', $args{'Lifecycle'})) 218 unless $self->ValidateLifecycle( $args{'Lifecycle'} ); 219 220 RT->DatabaseHandle->BeginTransaction(); 221 222 my ( $id, $msg ) = $self->SUPER::Create( 223 map { $_ => $args{$_} } qw(Name Description Lifecycle Disabled), 224 ); 225 unless ($id) { 226 RT->DatabaseHandle->Rollback(); 227 return (0, $self->loc("Catalog create failed: [_1]", $msg)); 228 } 229 230 # Create role groups 231 unless ($self->_CreateRoleGroups()) { 232 RT->Logger->error("Couldn't create role groups for catalog ". $self->id); 233 RT->DatabaseHandle->Rollback(); 234 return (0, $self->loc("Couldn't create role groups for catalog")); 235 } 236 237 # Figure out users for roles 238 my $roles = {}; 239 push @non_fatal_errors, $self->_ResolveRoles( $roles, %args ); 240 push @non_fatal_errors, $self->_AddRolesOnCreate( $roles, map { $_ => sub {1} } $self->Roles ); 241 242 # Create transaction 243 my ( $txn_id, $txn_msg, $txn ) = $self->_NewTransaction( Type => 'Create' ); 244 unless ($txn_id) { 245 RT->DatabaseHandle->Rollback(); 246 return (0, $self->loc( 'Catalog Create txn failed: [_1]', $txn_msg )); 247 } 248 249 $self->CacheNeedsUpdate(1); 250 RT->DatabaseHandle->Commit(); 251 252 return ($id, $self->loc('Catalog #[_1] created: [_2]', $self->id, $args{'Name'}), \@non_fatal_errors); 253} 254 255=head2 ValidateName NAME 256 257Requires that Names contain at least one non-digit and doesn't already exist. 258 259=cut 260 261sub ValidateName { 262 my $self = shift; 263 my $name = shift; 264 return 0 unless defined $name and length $name; 265 return 0 unless $name =~ /\D/; 266 267 my $catalog = RT::Catalog->new( RT->SystemUser ); 268 $catalog->Load($name); 269 return 0 if $catalog->id; 270 271 return 1; 272} 273 274=head2 Delete 275 276Catalogs may not be deleted. Always returns failure. 277 278You should disable the catalog instead using C<< $catalog->SetDisabled(1) >>. 279 280=cut 281 282sub Delete { 283 my $self = shift; 284 return (0, $self->loc("Catalogs may not be deleted")); 285} 286 287=head2 CurrentUserCanSee 288 289Returns true if the current user can see the catalog via the I<ShowCatalog> or 290I<AdminCatalog> rights. 291 292=cut 293 294sub CurrentUserCanSee { 295 my $self = shift; 296 return $self->CurrentUserHasRight('ShowCatalog') 297 || $self->CurrentUserHasRight('AdminCatalog'); 298} 299 300=head2 Owner 301 302Returns an L<RT::User> object for this catalog's I<Owner> role group. On error, 303returns undef. 304 305=head2 HeldBy 306 307Returns an L<RT::Group> object for this catalog's I<HeldBy> role group. The object 308may be unloaded if permissions aren't satisfied. 309 310=head2 Contacts 311 312Returns an L<RT::Group> object for this catalog's I<Contact> role 313group. The object may be unloaded if permissions aren't satisfied. 314 315=cut 316 317sub Owner { 318 my $self = shift; 319 my $group = $self->RoleGroup("Owner"); 320 return unless $group and $group->id; 321 return $group->UserMembersObj->First; 322} 323sub HeldBy { $_[0]->RoleGroup("HeldBy") } 324sub Contacts { $_[0]->RoleGroup("Contact") } 325 326=head2 AddRoleMember 327 328Checks I<AdminCatalog> before calling L<RT::Record::Role::Roles/_AddRoleMember>. 329 330=cut 331 332sub AddRoleMember { 333 my $self = shift; 334 335 return (0, $self->loc("No permission to modify this catalog")) 336 unless $self->CurrentUserHasRight("AdminCatalog"); 337 338 return $self->_AddRoleMember(@_); 339} 340 341=head2 DeleteRoleMember 342 343Checks I<AdminCatalog> before calling L<RT::Record::Role::Roles/_DeleteRoleMember>. 344 345=cut 346 347sub DeleteRoleMember { 348 my $self = shift; 349 350 return (0, $self->loc("No permission to modify this catalog")) 351 unless $self->CurrentUserHasRight("AdminCatalog"); 352 353 return $self->_DeleteRoleMember(@_); 354} 355 356=head2 RoleGroup 357 358An ACL'd version of L<RT::Record::Role::Roles/_RoleGroup>. Checks I<ShowCatalog>. 359 360=cut 361 362sub RoleGroup { 363 my $self = shift; 364 if ($self->CurrentUserCanSee) { 365 return $self->_RoleGroup(@_); 366 } else { 367 return RT::Group->new( $self->CurrentUser ); 368 } 369} 370 371=head2 AssetCustomFields 372 373Returns an L<RT::CustomFields> object containing all global and 374catalog-specific B<asset> custom fields. 375 376=cut 377 378sub AssetCustomFields { 379 my $self = shift; 380 my $cfs = RT::CustomFields->new( $self->CurrentUser ); 381 if ($self->CurrentUserCanSee) { 382 $cfs->SetContextObject( $self ); 383 $cfs->LimitToGlobalOrObjectId( $self->Id ); 384 $cfs->LimitToLookupType( RT::Asset->CustomFieldLookupType ); 385 $cfs->ApplySortOrder; 386 } else { 387 $cfs->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' ); 388 } 389 return ($cfs); 390} 391 392=head1 INTERNAL METHODS 393 394=head2 CacheNeedsUpdate 395 396Takes zero or one arguments. 397 398If a true argument is provided, marks any Catalog caches as needing an update. 399This happens when catalogs are created, disabled/enabled, or modified. Returns 400nothing. 401 402If no arguments are provided, returns an epoch time that any catalog caches 403should be newer than. 404 405May be called as a class or object method. 406 407=cut 408 409sub CacheNeedsUpdate { 410 my $class = shift; 411 my $update = shift; 412 413 if ($update) { 414 RT->System->SetAttribute(Name => 'CatalogCacheNeedsUpdate', Content => time); 415 return; 416 } else { 417 my $attribute = RT->System->FirstAttribute('CatalogCacheNeedsUpdate'); 418 return $attribute ? $attribute->Content : 0; 419 } 420} 421 422=head1 PRIVATE METHODS 423 424Documented for internal use only, do not call these from outside RT::Catalog 425itself. 426 427=head2 _Set 428 429Checks if the current user can I<AdminCatalog> before calling C<SUPER::_Set> 430and records a transaction against this object if C<SUPER::_Set> was 431successful. 432 433=cut 434 435sub _Set { 436 my $self = shift; 437 my %args = ( 438 Field => undef, 439 Value => undef, 440 @_ 441 ); 442 443 return (0, $self->loc("Permission Denied")) 444 unless $self->CurrentUserHasRight('AdminCatalog'); 445 446 my $old = $self->_Value( $args{'Field'} ); 447 448 my ($ok, $msg) = $self->SUPER::_Set(@_); 449 450 # Only record the transaction if the _Set worked 451 return ($ok, $msg) unless $ok; 452 453 my $txn_type = "Set"; 454 if ($args{'Field'} eq "Disabled") { 455 if (not $old and $args{'Value'}) { 456 $txn_type = "Disabled"; 457 } 458 elsif ($old and not $args{'Value'}) { 459 $txn_type = "Enabled"; 460 } 461 } 462 463 $self->CacheNeedsUpdate(1); 464 465 my ($txn_id, $txn_msg, $txn) = $self->_NewTransaction( 466 Type => $txn_type, 467 Field => $args{'Field'}, 468 NewValue => $args{'Value'}, 469 OldValue => $old, 470 ); 471 return ($txn_id, scalar $txn->BriefDescription); 472} 473 474=head2 Lifecycle [CONTEXT_OBJ] 475 476Returns the current value of Lifecycle. 477 478Provide an optional asset object as context to check role-level rights 479in addition to catalog-level rights for ShowCatalog and AdminCatalog. 480 481(In the database, Lifecycle is stored as varchar(32).) 482=cut 483 484sub Lifecycle { 485 my $self = shift; 486 my $context_obj = shift; 487 488 if ( $context_obj && $context_obj->CatalogObj->Id eq $self->Id && 489 ( $context_obj->CurrentUserHasRight('ShowCatalog') or $context_obj->CurrentUserHasRight('AdminCatalog') ) ) { 490 return ( $self->__Value('Lifecycle') ); 491 } 492 493 return ( $self->_Value('Lifecycle') ); 494} 495 496=head2 _Value 497 498Checks L</CurrentUserCanSee> before calling C<SUPER::_Value>. 499 500=cut 501 502sub _Value { 503 my $self = shift; 504 return unless $self->CurrentUserCanSee; 505 return $self->SUPER::_Value(@_); 506} 507 508sub Table { "Catalogs" } 509 510sub _CoreAccessible { 511 { 512 id => { read => 1, type => 'int(11)', default => '' }, 513 Name => { read => 1, type => 'varchar(255)', default => '', write => 1 }, 514 Description => { read => 1, type => 'varchar(255)', default => '', write => 1 }, 515 Lifecycle => { read => 1, type => 'varchar(32)', default => 'assets', write => 1 }, 516 Disabled => { read => 1, type => 'int(2)', default => '0', write => 1 }, 517 Creator => { read => 1, type => 'int(11)', default => '0', auto => 1 }, 518 Created => { read => 1, type => 'datetime', default => '', auto => 1 }, 519 LastUpdatedBy => { read => 1, type => 'int(11)', default => '0', auto => 1 }, 520 LastUpdated => { read => 1, type => 'datetime', default => '', auto => 1 }, 521 } 522} 523 524sub FindDependencies { 525 my $self = shift; 526 my ($walker, $deps) = @_; 527 528 $self->SUPER::FindDependencies($walker, $deps); 529 530 # Role groups( HeldBy, Contact) 531 my $objs = RT::Groups->new( $self->CurrentUser ); 532 $objs->Limit( FIELD => 'Domain', VALUE => 'RT::Catalog-Role', CASESENSITIVE => 0 ); 533 $objs->Limit( FIELD => 'Instance', VALUE => $self->Id ); 534 $deps->Add( in => $objs ); 535 536 # Custom Fields on assets _in_ this catalog 537 $objs = RT::ObjectCustomFields->new( $self->CurrentUser ); 538 $objs->Limit( FIELD => 'ObjectId', 539 OPERATOR => '=', 540 VALUE => $self->id, 541 ENTRYAGGREGATOR => 'OR' ); 542 $objs->Limit( FIELD => 'ObjectId', 543 OPERATOR => '=', 544 VALUE => 0, 545 ENTRYAGGREGATOR => 'OR' ); 546 my $cfs = $objs->Join( 547 ALIAS1 => 'main', 548 FIELD1 => 'CustomField', 549 TABLE2 => 'CustomFields', 550 FIELD2 => 'id', 551 ); 552 $objs->Limit( ALIAS => $cfs, 553 FIELD => 'LookupType', 554 OPERATOR => 'STARTSWITH', 555 VALUE => 'RT::Catalog-' ); 556 $deps->Add( in => $objs ); 557 558 # Assets 559 $objs = RT::Assets->new( $self->CurrentUser ); 560 $objs->Limit( FIELD => "Catalog", VALUE => $self->Id ); 561 $objs->{allow_deleted_search} = 1; 562 $deps->Add( in => $objs ); 563 564} 565 566sub PreInflate { 567 my $class = shift; 568 my ( $importer, $uid, $data ) = @_; 569 570 $class->SUPER::PreInflate( $importer, $uid, $data ); 571 $data->{Name} = $importer->Qualify( $data->{Name} ); 572 573 return if $importer->MergeBy( "Name", $class, $uid, $data ); 574 return 1; 575} 576 577RT::Base->_ImportOverlays(); 578 5791; 580