forked from shaba/openuds
Added administration audit and fixed some translations
This commit is contained in:
parent
8b3ad295cc
commit
d48747abff
@ -43,6 +43,8 @@ from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import ugettext as _
|
||||
from uds.core import VERSION, VERSION_STAMP
|
||||
|
||||
from . import log
|
||||
|
||||
from .handlers import (
|
||||
Handler,
|
||||
HandlerError,
|
||||
@ -61,8 +63,6 @@ if typing.TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = ['Handler', 'Dispatcher']
|
||||
|
||||
AUTH_TOKEN_HEADER = 'X-Auth-Token'
|
||||
|
||||
|
||||
@ -237,7 +237,9 @@ class Dispatcher(View):
|
||||
# Dinamycally import children of this package.
|
||||
package = 'methods'
|
||||
|
||||
pkgpath = os.path.join(os.path.dirname(typing.cast(str, sys.modules[__name__].__file__)), package)
|
||||
pkgpath = os.path.join(
|
||||
os.path.dirname(typing.cast(str, sys.modules[__name__].__file__)), package
|
||||
)
|
||||
for _, name, _ in pkgutil.iter_modules([pkgpath]):
|
||||
# __import__(__name__ + '.' + package + '.' + name, globals(), locals(), [], 0)
|
||||
importlib.import_module(
|
||||
|
@ -1,5 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2012-2021 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
@ -28,7 +27,7 @@
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
@author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
Author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
import typing
|
||||
import logging
|
||||
@ -42,6 +41,9 @@ from uds.core.util import net
|
||||
from uds.models import Authenticator, User
|
||||
from uds.core.managers import cryptoManager
|
||||
|
||||
from . import log
|
||||
|
||||
|
||||
# Not imported at runtime, just for type checking
|
||||
if typing.TYPE_CHECKING:
|
||||
from uds.core.util.request import ExtendedHttpRequestWithUser
|
||||
@ -127,8 +129,8 @@ class Handler:
|
||||
self,
|
||||
request: 'ExtendedHttpRequestWithUser',
|
||||
path: str,
|
||||
operation: str,
|
||||
params: typing.Any,
|
||||
method: str,
|
||||
params: typing.MutableMapping[str, typing.Any],
|
||||
*args: str,
|
||||
**kwargs
|
||||
):
|
||||
@ -147,7 +149,7 @@ class Handler:
|
||||
|
||||
self._request = request
|
||||
self._path = path
|
||||
self._operation = operation
|
||||
self._operation = method
|
||||
self._params = params
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
@ -178,6 +180,9 @@ class Handler:
|
||||
else:
|
||||
self._user = User() # Empty user for non authenticated handlers
|
||||
|
||||
# Keep track of the operation
|
||||
log.log_operation(self)
|
||||
|
||||
def headers(self) -> typing.Dict[str, str]:
|
||||
"""
|
||||
Returns the headers of the REST request (all)
|
||||
|
60
server/src/uds/REST/log.py
Normal file
60
server/src/uds/REST/log.py
Normal file
@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2022 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
Author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
import typing
|
||||
import datetime
|
||||
|
||||
from uds.models import Log, getSqlDatetime
|
||||
from uds.core.util import log, config
|
||||
from uds.core.jobs import Job
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .handlers import Handler
|
||||
|
||||
|
||||
def log_operation(handler: 'Handler', level: int = log.INFO):
|
||||
"""
|
||||
Logs a request
|
||||
"""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
|
||||
# Global log is used without owner nor type
|
||||
Log.objects.create(
|
||||
owner_id=0,
|
||||
owner_type=log.OWNER_TYPE_REST,
|
||||
created=getSqlDatetime(),
|
||||
level=level,
|
||||
source=log.REST,
|
||||
data=f'{handler._request.user.pretty_name}: [{handler._request.method}] {handler._request.path}'[
|
||||
:255
|
||||
],
|
||||
)
|
||||
|
@ -59,7 +59,7 @@ class Authenticators(ModelHandler):
|
||||
# Custom get method "search" that requires authenticator id
|
||||
custom_methods = [('search', True)]
|
||||
detail = {'users': Users, 'groups': Groups}
|
||||
save_fields = ['name', 'comments', 'tags', 'priority', 'small_name', 'visible', 'mfa_id']
|
||||
save_fields = ['name', 'comments', 'tags', 'priority', 'small_name', 'visible', '-mfa_id']
|
||||
|
||||
table_title = _('Authenticators')
|
||||
table_fields = [
|
||||
@ -140,7 +140,8 @@ class Authenticators(ModelHandler):
|
||||
)
|
||||
return g
|
||||
raise Exception() # Not found
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.info('Type not found: %s', e)
|
||||
raise NotFound('type not found')
|
||||
|
||||
def item_as_dict(self, item: Authenticator) -> typing.Dict[str, typing.Any]:
|
||||
@ -213,7 +214,7 @@ class Authenticators(ModelHandler):
|
||||
self, fields: typing.Dict[str, typing.Any]
|
||||
) -> None: # pylint: disable=too-many-branches,too-many-statements
|
||||
logger.debug(self._params)
|
||||
if fields['mfa_id']:
|
||||
if fields.get('mfa_id'):
|
||||
try:
|
||||
mfa = MFA.objects.get(
|
||||
uuid=processUuid(fields['mfa_id'])
|
||||
|
@ -115,7 +115,7 @@ class BaseModelHandler(Handler):
|
||||
},
|
||||
}
|
||||
if 'tab' in field:
|
||||
v['gui']['tab'] = field['tab']
|
||||
v['gui']['tab'] = _(field['tab'] or '')
|
||||
gui.append(v)
|
||||
return gui
|
||||
|
||||
@ -268,7 +268,10 @@ class BaseModelHandler(Handler):
|
||||
args: typing.Dict[str, str] = {}
|
||||
try:
|
||||
for key in fldList:
|
||||
args[key] = self._params[key]
|
||||
if key.startswith('-'): # optional
|
||||
args[key[1:]] = self._params.get(key[1:], '')
|
||||
else:
|
||||
args[key] = self._params[key]
|
||||
# del self._params[key]
|
||||
except KeyError as e:
|
||||
raise RequestError('needed parameter not found in data {0}'.format(e))
|
||||
|
@ -140,7 +140,11 @@ class Config:
|
||||
self.set(self._default)
|
||||
self._data = self._default
|
||||
except Exception as e:
|
||||
logger.info('Error accessing db config {0}.{1}'.format(self._section.name(), self._key))
|
||||
logger.info(
|
||||
'Error accessing db config {0}.{1}'.format(
|
||||
self._section.name(), self._key
|
||||
)
|
||||
)
|
||||
logger.exception(e)
|
||||
self._data = self._default
|
||||
|
||||
@ -298,7 +302,9 @@ class Config:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def getConfigValues(addCrypt: bool = False) -> typing.Mapping[str, typing.Mapping[str, typing.Mapping[str, typing.Any]]]:
|
||||
def getConfigValues(
|
||||
addCrypt: bool = False,
|
||||
) -> typing.Mapping[str, typing.Mapping[str, typing.Mapping[str, typing.Any]]]:
|
||||
"""
|
||||
Returns a dictionary with all config values
|
||||
"""
|
||||
@ -472,6 +478,11 @@ class GlobalConfig:
|
||||
'New Max restriction', '0', type=Config.BOOLEAN_FIELD
|
||||
)
|
||||
|
||||
# Maximum security logs duration in days
|
||||
MAX_AUDIT_LOGS_DURATION: Config.Value = Config.section(SECURITY_SECTION).value(
|
||||
'Max Audit Logs duration', '365', type=Config.NUMERIC_FIELD
|
||||
)
|
||||
|
||||
# Allowed "trusted sources" for request
|
||||
TRUSTED_SOURCES: Config.Value = Config.section(SECURITY_SECTION).value(
|
||||
'Trusted Hosts', '*', type=Config.TEXT_FIELD
|
||||
|
@ -48,7 +48,7 @@ OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (
|
||||
) # @UndefinedVariable
|
||||
|
||||
# Logging sources
|
||||
INTERNAL, ACTOR, TRANSPORT, OSMANAGER, UNKNOWN, WEB, ADMIN, SERVICE = (
|
||||
INTERNAL, ACTOR, TRANSPORT, OSMANAGER, UNKNOWN, WEB, ADMIN, SERVICE, REST = (
|
||||
'internal',
|
||||
'actor',
|
||||
'transport',
|
||||
@ -57,6 +57,7 @@ INTERNAL, ACTOR, TRANSPORT, OSMANAGER, UNKNOWN, WEB, ADMIN, SERVICE = (
|
||||
'web',
|
||||
'admin',
|
||||
'service',
|
||||
'rest',
|
||||
)
|
||||
|
||||
OTHERSTR, DEBUGSTR, INFOSTR, WARNSTR, ERRORSTR, FATALSTR = (
|
||||
@ -81,6 +82,10 @@ __nameLevels = {
|
||||
# Reverse dict of names
|
||||
__valueLevels = {v: k for k, v in __nameLevels.items()}
|
||||
|
||||
# Global log owner types:
|
||||
OWNER_TYPE_GLOBAL = -1
|
||||
OWNER_TYPE_REST = -2
|
||||
|
||||
|
||||
def logLevelFromStr(level: str) -> int:
|
||||
"""
|
||||
|
@ -32,12 +32,14 @@
|
||||
"""
|
||||
from importlib import import_module
|
||||
import logging
|
||||
import datetime
|
||||
import typing
|
||||
|
||||
from django.conf import settings
|
||||
from uds.core.util.cache import Cache
|
||||
from uds.core.jobs import Job
|
||||
from uds.models import TicketStore
|
||||
from uds.models import TicketStore, Log, getSqlDatetime
|
||||
from uds.core.util import config, log
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -65,7 +67,6 @@ class TicketStoreCleaner(Job):
|
||||
|
||||
|
||||
class SessionsCleaner(Job):
|
||||
|
||||
frecuency = 3600 * 24 * 7 # Once a week will be enough
|
||||
friendly_name = 'User Sessions cleaner'
|
||||
|
||||
@ -83,3 +84,20 @@ class SessionsCleaner(Job):
|
||||
pass # No problem if no cleanup
|
||||
|
||||
logger.debug('Done session cleanup')
|
||||
|
||||
|
||||
class AuditLogCleanup(Job):
|
||||
frecuency = 60 * 60 * 24 # Once a day
|
||||
friendly_name = 'Audit Log Cleanup'
|
||||
|
||||
def run(self) -> None:
|
||||
"""
|
||||
Cleans logs older than days
|
||||
"""
|
||||
Log.objects.filter(
|
||||
date__lt=getSqlDatetime()
|
||||
- datetime.timedelta(
|
||||
days=config.GlobalConfig.MAX_AUDIT_LOGS_DURATION.getInt()
|
||||
),
|
||||
owner_type=log.OWNER_TYPE_REST,
|
||||
).delete()
|
||||
|
@ -39,11 +39,18 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Log(models.Model):
|
||||
"""
|
||||
Log model associated with an object.
|
||||
"""Log model associated with an object.
|
||||
|
||||
This log is mainly used to keep track of log relative to objects
|
||||
(such as when a user access a machine, or information related to user logins/logout, errors, ...)
|
||||
|
||||
Note:
|
||||
owner_id can be 0, in wich case, the log is global (not related to any object)
|
||||
|
||||
if owner id is 0, these are valid owner_type values:
|
||||
-1: Global log
|
||||
-2: REST API log
|
||||
See :py:mod:`uds.core.util.log` for more information
|
||||
"""
|
||||
|
||||
owner_id = models.IntegerField(db_index=True, default=0)
|
||||
|
Loading…
Reference in New Issue
Block a user