1#!/usr/local/bin/perl 2# -- 3# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/ 4# -- 5# This program is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.txt. 17# -- 18 19use strict; 20use warnings; 21 22use File::Basename; 23use FindBin qw($RealBin); 24use lib dirname($RealBin); 25use lib dirname($RealBin) . '/Kernel/cpan-lib'; 26use lib dirname($RealBin) . '/Custom'; 27 28use File::Find(); 29use File::stat(); 30use Getopt::Long(); 31 32my $OTRSDirectory = dirname($RealBin); 33my $OTRSDirectoryLength = length($OTRSDirectory); 34 35my $OtrsUser = 'otrs'; # default: otrs 36my $WebGroup = ''; # Try to find a default from predefined group list, take the first match. 37 38WEBGROUP: 39for my $GroupCheck (qw(wwwrun apache www-data www _www)) { 40 my ($GroupName) = getgrnam $GroupCheck; 41 if ($GroupName) { 42 $WebGroup = $GroupName; 43 last WEBGROUP; 44 } 45} 46 47my $AdminGroup = 'root'; # default: root 48my ( $Help, $DryRun, $SkipArticleDir, @SkipRegex, $OtrsUserID, $WebGroupID, $AdminGroupID ); 49 50sub PrintUsage { 51 print <<EOF; 52 53Set OTRS file permissions. 54 55Usage: 56 otrs.SetPermissions.pl [--otrs-user=<OTRS_USER>] [--web-group=<WEB_GROUP>] [--admin-group=<ADMIN_GROUP>] [--skip-article-dir] [--skip-regex="REGEX"] [--dry-run] 57 58Options: 59 [--otrs-user=<OTRS_USER>] - OTRS user, defaults to 'otrs'. 60 [--web-group=<WEB_GROUP>] - Web server group ('_www', 'www-data' or similar), try to find a default. 61 [--admin-group=<ADMIN_GROUP>] - Admin group, defaults to 'root'. 62 [--skip-article-dir] - Skip var/article as it might take too long on some systems. 63 [--skip-regex="REGEX"] - Add another skip regex like "^/var/my/directory". Paths start with / but are relative to the OTRS directory. --skip-regex can be specified multiple times. 64 [--dry-run] - Only report, don't change. 65 [--help] - Display help for this command. 66 67Help: 68Using this script without any options it will try to detect the correct user and group settings needed for your setup. 69 70 otrs.SetPermissions.pl 71 72EOF 73 return; 74} 75 76# Files/directories that should be ignored and not recursed into. 77my @IgnoreFiles = ( 78 qr{^/\.git}smx, 79 qr{^/\.tidyall}smx, 80 qr{^/\.tx}smx, 81 qr{^/\.settings}smx, 82 qr{^/\.ssh}smx, 83 qr{^/\.gpg}smx, 84 qr{^/\.gnupg}smx, 85); 86 87# Files to be marked as executable. 88my @ExecutableFiles = ( 89 qr{\.(?:pl|psgi|sh)$}smx, 90 qr{^/var/git/hooks/(?:pre|post)-receive$}smx, 91); 92 93# Special files that must not be written by web server user. 94my @ProtectedFiles = ( 95 qr{^/\.fetchmailrc$}smx, 96 qr{^/\.procmailrc$}smx, 97); 98 99my $ExitStatus = 0; 100 101sub Run { 102 Getopt::Long::GetOptions( 103 'help' => \$Help, 104 'otrs-user=s' => \$OtrsUser, 105 'web-group=s' => \$WebGroup, 106 'admin-group=s' => \$AdminGroup, 107 'dry-run' => \$DryRun, 108 'skip-article-dir' => \$SkipArticleDir, 109 'skip-regex=s' => \@SkipRegex, 110 ); 111 112 if ( defined $Help ) { 113 PrintUsage(); 114 exit 0; 115 } 116 117 if ( $> != 0 ) { # $EFFECTIVE_USER_ID 118 print STDERR "ERROR: Please run this script as superuser (root).\n"; 119 exit 1; 120 } 121 122 # check params 123 $OtrsUserID = getpwnam $OtrsUser; 124 if ( !$OtrsUser || !defined $OtrsUserID ) { 125 print STDERR "ERROR: --otrs-user is missing or invalid.\n"; 126 exit 1; 127 } 128 $WebGroupID = getgrnam $WebGroup; 129 if ( !$WebGroup || !defined $WebGroupID ) { 130 print STDERR "ERROR: --web-group is missing or invalid.\n"; 131 exit 1; 132 } 133 $AdminGroupID = getgrnam $AdminGroup; 134 if ( !$AdminGroup || !defined $AdminGroupID ) { 135 print STDERR "ERROR: --admin-group is invalid.\n"; 136 exit 1; 137 } 138 if ( defined $SkipArticleDir ) { 139 push @IgnoreFiles, qr{^/var/article}smx; 140 } 141 for my $Regex (@SkipRegex) { 142 push @IgnoreFiles, qr{$Regex}smx; 143 } 144 145 print "Setting permissions on $OTRSDirectory\n"; 146 File::Find::find( 147 { 148 wanted => \&SetPermissions, 149 no_chdir => 1, 150 follow => 1, 151 }, 152 $OTRSDirectory, 153 ); 154 exit $ExitStatus; 155} 156 157sub SetPermissions { 158 159 # First get a canonical full filename 160 my $File = $File::Find::fullname; 161 162 # If the link is a dangling symbolic link, then fullname will be set to undef. 163 return if !defined $File; 164 165 # Make sure it is inside the OTRS directory to avoid following symlinks outside 166 if ( substr( $File, 0, $OTRSDirectoryLength ) ne $OTRSDirectory ) { 167 $File::Find::prune = 1; # don't descend into subdirectories 168 return; 169 } 170 171 # Now get a canonical relative filename under the OTRS directory 172 my $RelativeFile = substr( $File, $OTRSDirectoryLength ) || '/'; 173 174 for my $IgnoreRegex (@IgnoreFiles) { 175 if ( $RelativeFile =~ $IgnoreRegex ) { 176 $File::Find::prune = 1; # don't descend into subdirectories 177 print "Skipping $RelativeFile\n"; 178 return; 179 } 180 } 181 182 # Ok, get target permissions for file 183 SetFilePermissions( $File, $RelativeFile ); 184 185 return; 186} 187 188sub SetFilePermissions { 189 my ( $File, $RelativeFile ) = @_; 190 191 ## no critic (ProhibitLeadingZeros) 192 # Writable by default, owner OTRS and group webserver. 193 my ( $TargetPermission, $TargetUserID, $TargetGroupID ) = ( 0660, $OtrsUserID, $WebGroupID ); 194 if ( -d $File ) { 195 196 # SETGID for all directories so that both OTRS and the web server can write to the files. 197 # Other users should be able to read and cd to the directories. 198 $TargetPermission = 02775; 199 } 200 else { 201 # Executable bit for script files. 202 EXEXUTABLE_REGEX: 203 for my $ExecutableRegex (@ExecutableFiles) { 204 if ( $RelativeFile =~ $ExecutableRegex ) { 205 $TargetPermission = 0770; 206 last EXEXUTABLE_REGEX; 207 } 208 } 209 210 # Some files are protected and must not be written by webserver. Set admin group. 211 PROTECTED_REGEX: 212 for my $ProtectedRegex (@ProtectedFiles) { 213 if ( $RelativeFile =~ $ProtectedRegex ) { 214 $TargetPermission = -d $File ? 0750 : 0640; 215 $TargetGroupID = $AdminGroupID; 216 last PROTECTED_REGEX; 217 } 218 } 219 } 220 221 # Special treatment for toplevel folder: this must be readonly, 222 # otherwise procmail will refuse to read .procmailrc (see bug#9391). 223 if ( $RelativeFile eq '/' ) { 224 $TargetPermission = 0755; 225 } 226 227 # There seem to be cases when stat does not work on a dangling link, skip in this case. 228 my $Stat = File::stat::stat($File) || return; 229 if ( ( $Stat->mode() & 07777 ) != $TargetPermission ) { 230 if ( defined $DryRun ) { 231 print sprintf( 232 "$RelativeFile permissions %o -> %o\n", 233 $Stat->mode() & 07777, 234 $TargetPermission 235 ); 236 } 237 elsif ( !chmod( $TargetPermission, $File ) ) { 238 print STDERR sprintf( 239 "ERROR: could not change $RelativeFile permissions %o -> %o: $!\n", 240 $Stat->mode() & 07777, 241 $TargetPermission 242 ); 243 $ExitStatus = 1; 244 } 245 } 246 if ( ( $Stat->uid() != $TargetUserID ) || ( $Stat->gid() != $TargetGroupID ) ) { 247 if ( defined $DryRun ) { 248 print sprintf( 249 "$RelativeFile ownership %s:%s -> %s:%s\n", 250 $Stat->uid(), 251 $Stat->gid(), 252 $TargetUserID, 253 $TargetGroupID 254 ); 255 } 256 elsif ( !chown( $TargetUserID, $TargetGroupID, $File ) ) { 257 print STDERR sprintf( 258 "ERROR: could not change $RelativeFile ownership %s:%s -> %s:%s: $!\n", 259 $Stat->uid(), 260 $Stat->gid(), 261 $TargetUserID, 262 $TargetGroupID 263 ); 264 $ExitStatus = 1; 265 } 266 } 267 268 return; 269 ## use critic 270} 271 272Run(); 273