Projet

Général

Profil

0001-general-reduce-logging-infrastructure-do-not-expose-.patch

Frédéric Péters, 01 février 2022 16:28

Télécharger (19 ko)

Voir les différences:

Subject: [PATCH 1/8] general: reduce logging infrastructure, do not expose
 anything in UI (#61292)

 tests/admin_pages/test_settings.py |  22 ---
 tests/form_pages/test_formdata.py  |   6 +-
 tests/test_datasource_chrono.py    |  10 --
 tests/test_fc_auth.py              |   1 -
 tests/test_saml_auth.py            |   2 -
 wcs/admin/settings.py              |   8 --
 wcs/qommon/admin/logger.py         | 224 -----------------------------
 wcs/qommon/admin/settings.py       |   3 +-
 wcs/qommon/logger.py               |  49 -------
 wcs/qommon/publisher.py            |   2 +-
 10 files changed, 3 insertions(+), 324 deletions(-)
 delete mode 100644 wcs/qommon/admin/logger.py
tests/admin_pages/test_settings.py
1 1
import io
2
import logging
3 2
import os
4 3
import urllib.parse
5 4
import zipfile
......
726 725
    }
727 726

  
728 727

  
729
def test_settings_logs(pub):
730
    # reset logging state
731
    logging.shutdown()
732
    if os.path.exists(os.path.join(pub.app_dir, 'wcs.log')):
733
        os.unlink(os.path.join(pub.app_dir, 'wcs.log'))
734

  
735
    create_superuser(pub)
736
    app = login(get_app(pub))
737
    resp = app.get('/backoffice/settings/')
738
    assert 'Logs' in resp.text
739
    resp = resp.click('Logs')
740
    assert '<td class="message">login</td>' in resp.text
741

  
742
    resp = app.get('/backoffice/settings/debug_options')
743
    assert resp.form['logger'].checked is True
744
    resp.form['logger'].checked = False
745
    resp = resp.form.submit()
746
    resp = resp.follow()
747
    assert 'Logs' not in resp.text
748

  
749

  
750 728
def test_settings_geolocation(pub):
751 729
    create_superuser(pub)
752 730
    app = login(get_app(pub))
tests/form_pages/test_formdata.py
770 770
    user.name = 'Foo Baré'
771 771
    user.store()
772 772

  
773
    pub.cfg['debug'] = {'logger': True}
773
    pub.cfg['debug'] = {}
774 774
    pub.write_cfg()
775 775
    wf = Workflow(name='status')
776 776
    st1 = wf.add_status('Status1', 'st1')
......
814 814
        http_post_request.return_value = None, 200, 'null', None
815 815
        resp = resp.form.submit('button_export_to')
816 816
        assert http_post_request.call_count == 1
817
        if locale.getpreferredencoding() == 'UTF-8':
818
            assert caplog.records[-1].message == "file 'template.pdf' pushed to portfolio of 'Foo Baré'"
819
        else:  # Python < 3.7
820
            assert caplog.records[-1].message == "file 'template.pdf' pushed to portfolio of 'Foo Bar\xe9'"
821 817

  
822 818
    resp = resp.follow()  # $form/$id/create_doc
823 819
    resp = resp.follow()  # $form/$id/create_doc/
tests/test_datasource_chrono.py
1 1
import io
2 2
import json
3
import os
4 3
from unittest import mock
5 4

  
6 5
import pytest
......
24 23
    req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
25 24
    pub.set_app_dir(req)
26 25
    pub._set_request(req)
27

  
28
    with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
29
        fd.write(
30
            '''
31
[debug]
32
logger=true
33
'''
34
        )
35

  
36 26
    pub.load_site_options()
37 27

  
38 28
    return pub
tests/test_fc_auth.py
66 66
    # setup an hobo profile
67 67
    CmdCheckHobos().update_profile(PROFILE, pub)
68 68
    pub.cfg['users']['field_name'] = ['_prenoms', '_nom']
69
    pub.cfg['debug'] = {'logger': True}
70 69
    pub.user_class.wipe()
71 70
    pub.write_cfg()
72 71

  
tests/test_saml_auth.py
267 267

  
268 268
    CmdCheckHobos().update_profile(PROFILE, pub)
269 269

  
270
    pub.cfg['debug'] = {'logger': True}
271
    pub.write_cfg()
272 270
    pub.set_config()
273 271

  
274 272
    pub.role_class.wipe()
wcs/admin/settings.py
41 41
from wcs.qommon import _, errors, get_cfg, ident, misc, template
42 42
from wcs.qommon.admin.cfg import cfg_submit
43 43
from wcs.qommon.admin.emails import EmailsDirectory
44
from wcs.qommon.admin.logger import LoggerDirectory
45 44
from wcs.qommon.admin.menu import error_page
46 45
from wcs.qommon.admin.settings import SettingsDirectory as QommonSettingsDirectory
47 46
from wcs.qommon.admin.texts import TextsDirectory
......
518 517
        ('user-templates', 'user_templates'),
519 518
        ('data-sources', 'data_sources'),
520 519
        'wscalls',
521
        'logs',
522 520
        ('api-access', 'api_access'),
523 521
    ]
524 522

  
......
530 528
    filetypes = FileTypesDirectory()
531 529
    data_sources = NamedDataSourcesDirectory()
532 530
    wscalls = NamedWsCallsDirectory()
533
    logs = LoggerDirectory()
534 531
    api_access = ApiAccessDirectory()
535 532

  
536 533
    def _q_index(self):
......
619 616
                _('Debug Options'),
620 617
                _('Configure options useful for debugging'),
621 618
            )
622
            if get_cfg('debug', {}).get('logger', True):
623
                r += htmltext('<dt><a href="logs/">%s</a></dt> <dd>%s</dd>') % (
624
                    _('Logs'),
625
                    _('Access application log files'),
626
                )
627 619
            r += htmltext('</dl>')
628 620
            r += htmltext('</div>')
629 621

  
wcs/qommon/admin/logger.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2010  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
import os
18
import random
19

  
20
from quixote import get_publisher, get_request, get_response
21
from quixote.directory import Directory
22
from quixote.html import TemplateIO, htmltext
23

  
24
from .. import _, errors, logger
25
from ..admin.menu import error_page
26
from ..backoffice.menu import html_top
27

  
28

  
29
class ByUserDirectory(Directory):
30
    def _q_lookup(self, component):
31
        return ByUserPages(component)
32

  
33

  
34
class LoggerDirectory(Directory):
35
    _q_exports = ['', 'download', 'by_user']
36

  
37
    by_user = ByUserDirectory()
38

  
39
    def _q_index(self):
40
        get_response().breadcrumb.append(('logs/', _('Logs')))
41
        html_top('logs', title=_('Logs'))
42
        r = TemplateIO(html=False)
43
        request = get_request()
44
        logfile = str(request.get_field('logfile', get_publisher().APP_NAME + '.log'))
45
        if not logfile.startswith(str(get_publisher().APP_NAME + '.log')) or '/' in str(logfile):
46
            return error_page('logs/', _('Bad log file: %s') % logfile)
47
        logfilename = str(os.path.join(get_publisher().app_dir, logfile))
48

  
49
        if not os.path.exists(logfilename):
50
            r += _('Nothing to show')
51
        else:
52
            get_response().filter['sidebar'] = self.get_sidebar(logfile)
53

  
54
            user_color_keys = {}
55
            last_date = None
56
            r += htmltext('<table id="logs">\n')
57
            r += htmltext('<thead> <tr>')
58
            r += htmltext(' <th>%s</th>') % _('Time')
59
            r += htmltext(' <th>%s</th>') % _('User')
60
            r += htmltext(' <th>%s</th>') % _('Message')
61
            r += htmltext('<tr></thead>\n')
62
            r += htmltext('<tbody>\n')
63
            userlabels = {}
64
            with open(logfilename) as fd:
65
                for d in logger.parse_logstream(fd):
66
                    if not d:
67
                        continue
68

  
69
                    if d.get('user_id'):
70
                        user_color_key = d['user_id']
71
                        if user_color_key == 'anonymous':
72
                            user_color_key += d['ip']
73
                        if user_color_key not in user_color_keys:
74
                            user_color_keys[user_color_key] = ''.join(
75
                                ['%x' % random.randint(0xC, 0xF) for x in range(3)]
76
                            )
77
                        r += htmltext('<tr class="level-%s" style="background: #%s;">') % (
78
                            d['level'].lower(),
79
                            user_color_keys[user_color_key],
80
                        )
81
                    else:
82
                        r += htmltext('<tr class="level-%s">') % d['level'].lower()
83

  
84
                    if last_date != d['date']:
85
                        r += htmltext(' <td class="time">%s&nbsp;%s</td>') % (d['date'], d['hour'][:-4])
86
                        last_date = d['date']
87
                    else:
88
                        r += htmltext(' <td class="time">%s</td>') % (d['hour'][:-4])
89

  
90
                    user_id = d.get('user_id')
91
                    if not user_id:
92
                        userlabel = None
93
                    elif user_id == 'anonymous':
94
                        userlabel = _('Anonymous')
95
                        ip = d['ip']
96
                        r += htmltext(' <td class="userlabel"><span title="%s">%s</span></td>') % (
97
                            ip,
98
                            userlabel,
99
                        )
100
                    elif user_id == 'unlogged':
101
                        userlabel = _('Unlogged')
102
                        ip = d['ip']
103
                        r += htmltext(' <td class="userlabel"><span title="%s">%s</span></td>') % (
104
                            ip,
105
                            userlabel,
106
                        )
107
                    elif user_id == 'bot':
108
                        userlabel = _('Bot')
109
                        r += htmltext(' <td class="userlabel">%s</td>') % userlabel
110
                    else:
111
                        userlabel = userlabels.get(user_id)
112
                        if not userlabel:
113
                            try:
114
                                user = get_publisher().user_class.get(user_id)
115
                                userlabel = htmltext(user.display_name.replace(' ', '&nbsp;'))
116
                            except KeyError:
117
                                userlabel = _('Unknown')
118
                            userlabels[user_id] = userlabel
119
                        r += htmltext(' <td class="userlabel">%s</td>') % userlabel
120
                    if userlabel:
121
                        r += htmltext(' <td class="message">%s</td>') % d['message']
122
                    else:
123
                        r += htmltext('<td class="message" colspan="2">%s</td>') % d['message']
124
                    r += htmltext('</tr>\n')
125
            r += htmltext('</tbody>\n')
126
            r += htmltext('</table>\n')
127
        return r.getvalue()
128

  
129
    def get_sidebar(self, logfile):
130
        r = TemplateIO(html=True)
131
        r += htmltext('<ul>')
132
        if logfile:
133
            r += htmltext('<li><a href="download?logfile=%s">%s</a></li>') % (
134
                logfile,
135
                _('Download Raw Log File'),
136
            )
137
        else:
138
            r += htmltext('<li><a href="download">%s</a></li>') % _('Download Raw Log File')
139
        r += htmltext('</ul>')
140

  
141
        logfiles = [
142
            x for x in os.listdir(get_publisher().app_dir) if x.startswith(get_publisher().APP_NAME + '.log')
143
        ]
144
        if len(logfiles) > 1:
145
            options = []
146
            for lfile in logfiles:
147
                with open(os.path.join(get_publisher().app_dir, lfile)) as fd:
148
                    firstline = fd.readline()
149
                d = logger.readline(firstline)
150
                if not d:
151
                    continue
152
                if logfile == lfile:
153
                    selected = 'selected="selected" '
154
                else:
155
                    selected = ''
156
                options.append(
157
                    {'selected': selected, 'lfile': lfile, 'date': '%s %s' % (d['date'], d['hour'])}
158
                )
159

  
160
            r += htmltext('<form id="other-log-select">')
161
            r += _('Select another logfile:')
162
            r += htmltext('<select name="logfile">')
163
            options.sort(key=lambda x: x['date'])
164
            options.reverse()
165
            for option in options:
166
                option['since'] = str(_('Since: %s') % option['date'])[:-4]
167
                r += htmltext('<option value="%(lfile)s"%(selected)s>%(since)s</option>') % option
168
            r += htmltext('</select>')
169
            r += htmltext('<input type="submit" value="%s" />') % _('Submit')
170

  
171
        return r.getvalue()
172

  
173
    def download(self):
174
        request = get_request()
175
        logfile = request.get_field('logfile', get_publisher().APP_NAME + '.log')
176
        if not logfile.startswith(get_publisher().APP_NAME + '.log') or '/' in logfile:
177
            return error_page('logs/', _('Bad log file: %s') % logfile)
178
        logfilename = os.path.join(get_publisher().app_dir, logfile)
179
        response = get_response()
180
        response.set_content_type('text/x-log', 'iso-8859-1')
181
        response.set_header('content-disposition', 'attachment; filename=%s' % logfile)
182
        with open(logfilename) as fd:
183
            return fd.read()
184

  
185

  
186
class ByUserPages(Directory):
187
    _q_exports = ['']
188

  
189
    def __init__(self, component):
190
        try:
191
            self.user = get_publisher().user_class.get(component)
192
        except KeyError:
193
            raise errors.TraversalError()
194

  
195
    def _q_index(self):
196
        html_top('logs', title=_('Logs'))
197
        r = TemplateIO(html=True)
198
        r += htmltext('<h2>%s - %s</h2>') % (_('User'), self.user.name)
199

  
200
        last_date = None
201
        r += htmltext('<table id="logs">')
202
        r += htmltext('<thead> <tr>')
203
        r += htmltext(' <th>%s</th>') % _('Time')
204
        r += htmltext(' <th>%s</th>') % _('Message')
205
        r += htmltext('<tr></thead>')
206
        r += htmltext('<tbody>')
207
        logfilename = str(os.path.join(get_publisher().app_dir, get_publisher().APP_NAME + '.log'))
208
        if os.path.exists(logfilename):
209
            with open(logfilename) as fd:
210
                for line in fd:
211
                    d = logger.readline(line)
212
                    if not d or d['user_id'] != str(self.user.id):
213
                        continue
214
                    r += htmltext('<tr>')
215
                    if last_date != d['date']:
216
                        r += htmltext(' <td class="time">%s&nbsp;%s</td>') % (d['date'], d['hour'][:-4])
217
                        last_date = d['date']
218
                    else:
219
                        r += htmltext(' <td class="time">%s</td>') % (d['hour'][:-4])
220
                    r += htmltext(' <td><a href="%s">%s</a></td>') % (d['url'], d['message'])
221
                    r += htmltext('</tr>')
222
        r += htmltext('</tbody>')
223
        r += htmltext('</table>')
224
        return r.getvalue()
wcs/qommon/admin/settings.py
73 73
            title=_('Email for Tracebacks'),
74 74
            value=debug_cfg.get('error_email', ''),
75 75
        )
76
        form.add(CheckboxWidget, 'logger', title=_('Logger'), value=debug_cfg.get('logger', True))
77 76
        form.add(
78 77
            CheckboxWidget,
79 78
            'debug_mode',
......
104 103
            cfg_submit(
105 104
                form,
106 105
                'debug',
107
                ('error_email', 'logger', 'debug_mode', 'mail_redirection'),
106
                ('error_email', 'debug_mode', 'mail_redirection'),
108 107
            )
109 108
            return redirect('.')
wcs/qommon/logger.py
97 97
        ) or '[nosession]'
98 98

  
99 99
        return logging.Formatter.format(self, record).replace('\n', '\n ')
100

  
101

  
102
def parse_logstream(stream):
103
    """
104
    Parse a stream of lines making a log file, each log line start with a
105
    non-blank character continue until the lines does not start with a blank
106
    character
107
    """
108
    line = next(stream)
109
    while True:
110
        r = readline(line)
111
        try:
112
            # Skip badly formatted lines
113
            if r is None:
114
                line = next(stream)
115
                continue
116
            while True:
117
                line = next(stream)
118
                if line.startswith(' '):
119
                    # Append the line without the first blank
120
                    r['message'] = r['message'] + line[1:]
121
                    continue
122
                break
123
        except StopIteration:
124
            # Ont last line return the current one
125
            if r is not None:
126
                r['message'] = r['message'].strip()
127
                yield r
128
            break
129
        r['message'] = r['message'].strip()
130
        yield r
131

  
132

  
133
def readline(line):
134
    if not line:
135
        return None
136
    try:
137
        date, hour, level, ip, session_id, url, user_id, dash, message = line.split(' ', 8)
138
    except ValueError:
139
        try:
140
            date, hour, level, message = line.split(' ', 3)
141
        except ValueError:
142
            return None  # misformatted line
143
        del line
144
        return locals()
145
    if dash != '-':
146
        return None
147
    del line
148
    return locals()
wcs/qommon/publisher.py
694 694
            cls.root_directory_class(),
695 695
            session_cookie_name=cls.APP_NAME,
696 696
            session_cookie_path='/',
697
            logger=logger.ApplicationLogger(error_log=cls.ERROR_LOG),
697
            logger=logger.ApplicationLogger(),
698 698
        )
699 699
        publisher.substitutions = Substitutions()
700 700
        publisher.app_dir = cls.APP_DIR
701
-