##// END OF EJS Templates
artifacts: added reading of metadata and define basic audit-log entries for add/delete artifact
marcink -
r3679:4cc4558e new-ui
parent child Browse files
Show More
@@ -1,211 +1,224 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import time
23 23 import shutil
24 24 import hashlib
25 25
26 26 from rhodecode.lib.ext_json import json
27 27 from rhodecode.apps.file_store import utils
28 28 from rhodecode.apps.file_store.extensions import resolve_extensions
29 29 from rhodecode.apps.file_store.exceptions import FileNotAllowedException
30 30
31 31 METADATA_VER = 'v1'
32 32
33 33
34 34 class LocalFileStorage(object):
35 35
36 36 @classmethod
37 37 def resolve_name(cls, name, directory):
38 38 """
39 39 Resolves a unique name and the correct path. If a filename
40 40 for that path already exists then a numeric prefix with values > 0 will be
41 41 added, for example test.jpg -> test-1.jpg etc. initially file would have 0 prefix.
42 42
43 43 :param name: base name of file
44 44 :param directory: absolute directory path
45 45 """
46 46
47 47 basename, ext = os.path.splitext(name)
48 48 counter = 0
49 49 while True:
50 50 name = '%s-%d%s' % (basename, counter, ext)
51 51
52 52 # sub_store prefix to optimize disk usage, e.g some_path/ab/final_file
53 53 sub_store = cls._sub_store_from_filename(basename)
54 54 sub_store_path = os.path.join(directory, sub_store)
55 55 if not os.path.exists(sub_store_path):
56 56 os.makedirs(sub_store_path)
57 57
58 58 path = os.path.join(sub_store_path, name)
59 59 if not os.path.exists(path):
60 60 return name, path
61 61 counter += 1
62 62
63 63 @classmethod
64 64 def _sub_store_from_filename(cls, filename):
65 65 return filename[:2]
66 66
67 67 @classmethod
68 68 def calculate_path_hash(cls, file_path):
69 69 """
70 70 Efficient calculation of file_path sha256 sum
71 71
72 72 :param file_path:
73 73 :return: sha256sum
74 74 """
75 75 digest = hashlib.sha256()
76 76 with open(file_path, 'rb') as f:
77 77 for chunk in iter(lambda: f.read(1024 * 100), b""):
78 78 digest.update(chunk)
79 79
80 80 return digest.hexdigest()
81 81
82 82 def __init__(self, base_path, extension_groups=None):
83 83
84 84 """
85 85 Local file storage
86 86
87 87 :param base_path: the absolute base path where uploads are stored
88 88 :param extension_groups: extensions string
89 89 """
90 90
91 91 extension_groups = extension_groups or ['any']
92 92 self.base_path = base_path
93 93 self.extensions = resolve_extensions([], groups=extension_groups)
94 94
95 95 def store_path(self, filename):
96 96 """
97 97 Returns absolute file path of the filename, joined to the
98 98 base_path.
99 99
100 100 :param filename: base name of file
101 101 """
102 102 sub_store = self._sub_store_from_filename(filename)
103 103 return os.path.join(self.base_path, sub_store, filename)
104 104
105 105 def delete(self, filename):
106 106 """
107 107 Deletes the filename. Filename is resolved with the
108 108 absolute path based on base_path. If file does not exist,
109 109 returns **False**, otherwise **True**
110 110
111 111 :param filename: base name of file
112 112 """
113 113 if self.exists(filename):
114 114 os.remove(self.store_path(filename))
115 115 return True
116 116 return False
117 117
118 118 def exists(self, filename):
119 119 """
120 120 Checks if file exists. Resolves filename's absolute
121 121 path based on base_path.
122 122
123 123 :param filename: base name of file
124 124 """
125 125 return os.path.exists(self.store_path(filename))
126 126
127 127 def filename_allowed(self, filename, extensions=None):
128 128 """Checks if a filename has an allowed extension
129 129
130 130 :param filename: base name of file
131 131 :param extensions: iterable of extensions (or self.extensions)
132 132 """
133 133 _, ext = os.path.splitext(filename)
134 134 return self.extension_allowed(ext, extensions)
135 135
136 136 def extension_allowed(self, ext, extensions=None):
137 137 """
138 138 Checks if an extension is permitted. Both e.g. ".jpg" and
139 139 "jpg" can be passed in. Extension lookup is case-insensitive.
140 140
141 141 :param ext: extension to check
142 142 :param extensions: iterable of extensions to validate against (or self.extensions)
143 143 """
144 144
145 145 extensions = extensions or self.extensions
146 146 if not extensions:
147 147 return True
148 148 if ext.startswith('.'):
149 149 ext = ext[1:]
150 150 return ext.lower() in extensions
151 151
152 152 def save_file(self, file_obj, filename, directory=None, extensions=None,
153 153 extra_metadata=None, **kwargs):
154 154 """
155 155 Saves a file object to the uploads location.
156 156 Returns the resolved filename, i.e. the directory +
157 157 the (randomized/incremented) base name.
158 158
159 159 :param file_obj: **cgi.FieldStorage** object (or similar)
160 160 :param filename: original filename
161 161 :param directory: relative path of sub-directory
162 162 :param extensions: iterable of allowed extensions, if not default
163 163 :param extra_metadata: extra JSON metadata to store next to the file with .meta suffix
164 164 """
165 165
166 166 extensions = extensions or self.extensions
167 167
168 168 if not self.filename_allowed(filename, extensions):
169 169 raise FileNotAllowedException()
170 170
171 171 if directory:
172 172 dest_directory = os.path.join(self.base_path, directory)
173 173 else:
174 174 dest_directory = self.base_path
175 175
176 176 if not os.path.exists(dest_directory):
177 177 os.makedirs(dest_directory)
178 178
179 179 filename = utils.uid_filename(filename)
180 180
181 181 # resolve also produces special sub-dir for file optimized store
182 182 filename, path = self.resolve_name(filename, dest_directory)
183 183 stored_file_dir = os.path.dirname(path)
184 184
185 185 file_obj.seek(0)
186 186
187 187 with open(path, "wb") as dest:
188 188 shutil.copyfileobj(file_obj, dest)
189 189
190 190 metadata = {}
191 191 if extra_metadata:
192 192 metadata = extra_metadata
193 193
194 194 size = os.stat(path).st_size
195 195 file_hash = self.calculate_path_hash(path)
196 196
197 197 metadata.update(
198 198 {"filename": filename,
199 199 "size": size,
200 200 "time": time.time(),
201 201 "sha256": file_hash,
202 202 "meta_ver": METADATA_VER})
203 203
204 204 filename_meta = filename + '.meta'
205 205 with open(os.path.join(stored_file_dir, filename_meta), "wb") as dest_meta:
206 206 dest_meta.write(json.dumps(metadata))
207 207
208 208 if directory:
209 209 filename = os.path.join(directory, filename)
210 210
211 211 return filename, metadata
212
213 def get_metadata(self, filename):
214 """
215 Reads JSON stored metadata for a file
216
217 :param filename:
218 :return:
219 """
220 filename = self.store_path(filename)
221 filename_meta = filename + '.meta'
222
223 with open(filename_meta, "rb") as source_meta:
224 return json.loads(source_meta.read())
@@ -1,144 +1,144 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import logging
21 21
22 22 from pyramid.view import view_config
23 23 from pyramid.response import FileResponse
24 24 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
25 25
26 26 from rhodecode.apps._base import BaseAppView
27 27 from rhodecode.apps.file_store import utils
28 28 from rhodecode.apps.file_store.exceptions import (
29 29 FileNotAllowedException, FileOverSizeException)
30 30
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib import audit_logger
33 33 from rhodecode.lib.auth import (CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny)
34 34 from rhodecode.model.db import Session, FileStore
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 class FileStoreView(BaseAppView):
40 40 upload_key = 'store_file'
41 41
42 42 def load_default_context(self):
43 43 c = self._get_local_tmpl_context()
44 44 self.storage = utils.get_file_storage(self.request.registry.settings)
45 45 return c
46 46
47 47 @NotAnonymous()
48 48 @CSRFRequired()
49 49 @view_config(route_name='upload_file', request_method='POST', renderer='json_ext')
50 50 def upload_file(self):
51 51 self.load_default_context()
52 52 file_obj = self.request.POST.get(self.upload_key)
53 53
54 54 if file_obj is None:
55 55 return {'store_fid': None,
56 56 'access_path': None,
57 57 'error': '{} data field is missing'.format(self.upload_key)}
58 58
59 59 if not hasattr(file_obj, 'filename'):
60 60 return {'store_fid': None,
61 61 'access_path': None,
62 62 'error': 'filename cannot be read from the data field'}
63 63
64 64 filename = file_obj.filename
65 65
66 66 metadata = {
67 67 'user_uploaded': {'username': self._rhodecode_user.username,
68 68 'user_id': self._rhodecode_user.user_id,
69 69 'ip': self._rhodecode_user.ip_addr}}
70 70 try:
71 store_fid, metadata = self.storage.save_file(
71 store_uid, metadata = self.storage.save_file(
72 72 file_obj.file, filename, extra_metadata=metadata)
73 73 except FileNotAllowedException:
74 74 return {'store_fid': None,
75 75 'access_path': None,
76 76 'error': 'File {} is not allowed.'.format(filename)}
77 77
78 78 except FileOverSizeException:
79 79 return {'store_fid': None,
80 80 'access_path': None,
81 81 'error': 'File {} is exceeding allowed limit.'.format(filename)}
82 82
83 83 try:
84 84 entry = FileStore.create(
85 file_uid=store_fid, filename=metadata["filename"],
85 file_uid=store_uid, filename=metadata["filename"],
86 86 file_hash=metadata["sha256"], file_size=metadata["size"],
87 87 file_description='upload attachment',
88 88 check_acl=False, user_id=self._rhodecode_user.user_id
89 89 )
90 90 Session().add(entry)
91 91 Session().commit()
92 92 log.debug('Stored upload in DB as %s', entry)
93 93 except Exception:
94 94 log.exception('Failed to store file %s', filename)
95 95 return {'store_fid': None,
96 96 'access_path': None,
97 97 'error': 'File {} failed to store in DB.'.format(filename)}
98 98
99 return {'store_fid': store_fid,
100 'access_path': h.route_path('download_file', fid=store_fid)}
99 return {'store_fid': store_uid,
100 'access_path': h.route_path('download_file', fid=store_uid)}
101 101
102 102 @view_config(route_name='download_file')
103 103 def download_file(self):
104 104 self.load_default_context()
105 105 file_uid = self.request.matchdict['fid']
106 106 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
107 107
108 108 if not self.storage.exists(file_uid):
109 109 log.debug('File with FID:%s not found in the store', file_uid)
110 110 raise HTTPNotFound()
111 111
112 112 db_obj = FileStore().query().filter(FileStore.file_uid == file_uid).scalar()
113 113 if not db_obj:
114 114 raise HTTPNotFound()
115 115
116 116 # private upload for user
117 117 if db_obj.check_acl and db_obj.scope_user_id:
118 118 user = db_obj.user
119 119 if self._rhodecode_db_user.user_id != user.user_id:
120 120 log.warning('Access to file store object forbidden')
121 121 raise HTTPNotFound()
122 122
123 123 # scoped to repository permissions
124 124 if db_obj.check_acl and db_obj.scope_repo_id:
125 125 repo = db_obj.repo
126 126 perm_set = ['repository.read', 'repository.write', 'repository.admin']
127 127 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
128 128 if not has_perm:
129 129 log.warning('Access to file store object forbidden')
130 130 raise HTTPNotFound()
131 131
132 132 # scoped to repository group permissions
133 133 if db_obj.check_acl and db_obj.scope_repo_group_id:
134 134 repo_group = db_obj.repo_group
135 135 perm_set = ['group.read', 'group.write', 'group.admin']
136 136 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
137 137 if not has_perm:
138 138 log.warning('Access to file store object forbidden')
139 139 raise HTTPNotFound()
140 140
141 141 FileStore.bump_access_counter(file_uid)
142 142
143 143 file_path = self.storage.store_path(file_uid)
144 144 return FileResponse(file_path)
@@ -1,288 +1,291 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2017-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23
24 24 from rhodecode.lib.jsonalchemy import JsonRaw
25 25 from rhodecode.model import meta
26 26 from rhodecode.model.db import User, UserLog, Repository
27 27
28 28
29 29 log = logging.getLogger(__name__)
30 30
31 31 # action as key, and expected action_data as value
32 32 ACTIONS_V1 = {
33 33 'user.login.success': {'user_agent': ''},
34 34 'user.login.failure': {'user_agent': ''},
35 35 'user.logout': {'user_agent': ''},
36 36 'user.register': {},
37 37 'user.password.reset_request': {},
38 38 'user.push': {'user_agent': '', 'commit_ids': []},
39 39 'user.pull': {'user_agent': ''},
40 40
41 41 'user.create': {'data': {}},
42 42 'user.delete': {'old_data': {}},
43 43 'user.edit': {'old_data': {}},
44 44 'user.edit.permissions': {},
45 45 'user.edit.ip.add': {'ip': {}, 'user': {}},
46 46 'user.edit.ip.delete': {'ip': {}, 'user': {}},
47 47 'user.edit.token.add': {'token': {}, 'user': {}},
48 48 'user.edit.token.delete': {'token': {}, 'user': {}},
49 49 'user.edit.email.add': {'email': ''},
50 50 'user.edit.email.delete': {'email': ''},
51 51 'user.edit.ssh_key.add': {'token': {}, 'user': {}},
52 52 'user.edit.ssh_key.delete': {'token': {}, 'user': {}},
53 53 'user.edit.password_reset.enabled': {},
54 54 'user.edit.password_reset.disabled': {},
55 55
56 56 'user_group.create': {'data': {}},
57 57 'user_group.delete': {'old_data': {}},
58 58 'user_group.edit': {'old_data': {}},
59 59 'user_group.edit.permissions': {},
60 60 'user_group.edit.member.add': {'user': {}},
61 61 'user_group.edit.member.delete': {'user': {}},
62 62
63 63 'repo.create': {'data': {}},
64 64 'repo.fork': {'data': {}},
65 65 'repo.edit': {'old_data': {}},
66 66 'repo.edit.permissions': {},
67 67 'repo.edit.permissions.branch': {},
68 68 'repo.archive': {'old_data': {}},
69 69 'repo.delete': {'old_data': {}},
70 70
71 71 'repo.archive.download': {'user_agent': '', 'archive_name': '',
72 72 'archive_spec': '', 'archive_cached': ''},
73 73
74 74 'repo.permissions.branch_rule.create': {},
75 75 'repo.permissions.branch_rule.edit': {},
76 76 'repo.permissions.branch_rule.delete': {},
77 77
78 78 'repo.pull_request.create': '',
79 79 'repo.pull_request.edit': '',
80 80 'repo.pull_request.delete': '',
81 81 'repo.pull_request.close': '',
82 82 'repo.pull_request.merge': '',
83 83 'repo.pull_request.vote': '',
84 84 'repo.pull_request.comment.create': '',
85 85 'repo.pull_request.comment.delete': '',
86 86
87 87 'repo.pull_request.reviewer.add': '',
88 88 'repo.pull_request.reviewer.delete': '',
89 89
90 90 'repo.commit.strip': {'commit_id': ''},
91 91 'repo.commit.comment.create': {'data': {}},
92 92 'repo.commit.comment.delete': {'data': {}},
93 93 'repo.commit.vote': '',
94 94
95 'repo.artifact.add': '',
96 'repo.artifact.delete': '',
97
95 98 'repo_group.create': {'data': {}},
96 99 'repo_group.edit': {'old_data': {}},
97 100 'repo_group.edit.permissions': {},
98 101 'repo_group.delete': {'old_data': {}},
99 102 }
100 103
101 104 ACTIONS = ACTIONS_V1
102 105
103 106 SOURCE_WEB = 'source_web'
104 107 SOURCE_API = 'source_api'
105 108
106 109
107 110 class UserWrap(object):
108 111 """
109 112 Fake object used to imitate AuthUser
110 113 """
111 114
112 115 def __init__(self, user_id=None, username=None, ip_addr=None):
113 116 self.user_id = user_id
114 117 self.username = username
115 118 self.ip_addr = ip_addr
116 119
117 120
118 121 class RepoWrap(object):
119 122 """
120 123 Fake object used to imitate RepoObject that audit logger requires
121 124 """
122 125
123 126 def __init__(self, repo_id=None, repo_name=None):
124 127 self.repo_id = repo_id
125 128 self.repo_name = repo_name
126 129
127 130
128 131 def _store_log(action_name, action_data, user_id, username, user_data,
129 132 ip_address, repository_id, repository_name):
130 133 user_log = UserLog()
131 134 user_log.version = UserLog.VERSION_2
132 135
133 136 user_log.action = action_name
134 137 user_log.action_data = action_data or JsonRaw(u'{}')
135 138
136 139 user_log.user_ip = ip_address
137 140
138 141 user_log.user_id = user_id
139 142 user_log.username = username
140 143 user_log.user_data = user_data or JsonRaw(u'{}')
141 144
142 145 user_log.repository_id = repository_id
143 146 user_log.repository_name = repository_name
144 147
145 148 user_log.action_date = datetime.datetime.now()
146 149
147 150 return user_log
148 151
149 152
150 153 def store_web(*args, **kwargs):
151 154 if 'action_data' not in kwargs:
152 155 kwargs['action_data'] = {}
153 156 kwargs['action_data'].update({
154 157 'source': SOURCE_WEB
155 158 })
156 159 return store(*args, **kwargs)
157 160
158 161
159 162 def store_api(*args, **kwargs):
160 163 if 'action_data' not in kwargs:
161 164 kwargs['action_data'] = {}
162 165 kwargs['action_data'].update({
163 166 'source': SOURCE_API
164 167 })
165 168 return store(*args, **kwargs)
166 169
167 170
168 171 def store(action, user, action_data=None, user_data=None, ip_addr=None,
169 172 repo=None, sa_session=None, commit=False):
170 173 """
171 174 Audit logger for various actions made by users, typically this
172 175 results in a call such::
173 176
174 177 from rhodecode.lib import audit_logger
175 178
176 179 audit_logger.store(
177 180 'repo.edit', user=self._rhodecode_user)
178 181 audit_logger.store(
179 182 'repo.delete', action_data={'data': repo_data},
180 183 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'))
181 184
182 185 # repo action
183 186 audit_logger.store(
184 187 'repo.delete',
185 188 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'),
186 189 repo=audit_logger.RepoWrap(repo_name='some-repo'))
187 190
188 191 # repo action, when we know and have the repository object already
189 192 audit_logger.store(
190 193 'repo.delete', action_data={'source': audit_logger.SOURCE_WEB, },
191 194 user=self._rhodecode_user,
192 195 repo=repo_object)
193 196
194 197 # alternative wrapper to the above
195 198 audit_logger.store_web(
196 199 'repo.delete', action_data={},
197 200 user=self._rhodecode_user,
198 201 repo=repo_object)
199 202
200 203 # without an user ?
201 204 audit_logger.store(
202 205 'user.login.failure',
203 206 user=audit_logger.UserWrap(
204 207 username=self.request.params.get('username'),
205 208 ip_addr=self.request.remote_addr))
206 209
207 210 """
208 211 from rhodecode.lib.utils2 import safe_unicode
209 212 from rhodecode.lib.auth import AuthUser
210 213
211 214 action_spec = ACTIONS.get(action, None)
212 215 if action_spec is None:
213 216 raise ValueError('Action `{}` is not supported'.format(action))
214 217
215 218 if not sa_session:
216 219 sa_session = meta.Session()
217 220
218 221 try:
219 222 username = getattr(user, 'username', None)
220 223 if not username:
221 224 pass
222 225
223 226 user_id = getattr(user, 'user_id', None)
224 227 if not user_id:
225 228 # maybe we have username ? Try to figure user_id from username
226 229 if username:
227 230 user_id = getattr(
228 231 User.get_by_username(username), 'user_id', None)
229 232
230 233 ip_addr = ip_addr or getattr(user, 'ip_addr', None)
231 234 if not ip_addr:
232 235 pass
233 236
234 237 if not user_data:
235 238 # try to get this from the auth user
236 239 if isinstance(user, AuthUser):
237 240 user_data = {
238 241 'username': user.username,
239 242 'email': user.email,
240 243 }
241 244
242 245 repository_name = getattr(repo, 'repo_name', None)
243 246 repository_id = getattr(repo, 'repo_id', None)
244 247 if not repository_id:
245 248 # maybe we have repo_name ? Try to figure repo_id from repo_name
246 249 if repository_name:
247 250 repository_id = getattr(
248 251 Repository.get_by_repo_name(repository_name), 'repo_id', None)
249 252
250 253 action_name = safe_unicode(action)
251 254 ip_address = safe_unicode(ip_addr)
252 255
253 256 with sa_session.no_autoflush:
254 257 update_user_last_activity(sa_session, user_id)
255 258
256 259 user_log = _store_log(
257 260 action_name=action_name,
258 261 action_data=action_data or {},
259 262 user_id=user_id,
260 263 username=username,
261 264 user_data=user_data or {},
262 265 ip_address=ip_address,
263 266 repository_id=repository_id,
264 267 repository_name=repository_name
265 268 )
266 269
267 270 sa_session.add(user_log)
268 271
269 272 if commit:
270 273 sa_session.commit()
271 274
272 275 entry_id = user_log.entry_id or ''
273 276 log.info('AUDIT[%s]: Logging action: `%s` by user:id:%s[%s] ip:%s',
274 277 entry_id, action_name, user_id, username, ip_address)
275 278
276 279 except Exception:
277 280 log.exception('AUDIT: failed to store audit log')
278 281
279 282
280 283 def update_user_last_activity(sa_session, user_id):
281 284 _last_activity = datetime.datetime.now()
282 285 try:
283 286 sa_session.query(User).filter(User.user_id == user_id).update(
284 287 {"last_activity": _last_activity})
285 288 log.debug(
286 289 'updated user `%s` last activity to:%s', user_id, _last_activity)
287 290 except Exception:
288 291 log.exception("Failed last activity update")
General Comments 0
You need to be logged in to leave comments. Login now