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