forked from shaba/openuds
Added token alias to secure unmanaged machine token
This commit is contained in:
parent
11f6eec913
commit
eeae98ca79
@ -270,6 +270,7 @@ class UDSServerApi(UDSApi):
|
|||||||
)
|
)
|
||||||
if r['os']
|
if r['os']
|
||||||
else None,
|
else None,
|
||||||
|
alias_token=r.get('alias_token'), # Possible alias for unmanaged
|
||||||
)
|
)
|
||||||
|
|
||||||
def ready(
|
def ready(
|
||||||
|
@ -274,7 +274,8 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Only removes master token for managed machines (will need it on next client execution)
|
# Only removes master token for managed machines (will need it on next client execution)
|
||||||
master_token = None if self.isManaged() else self._cfg.master_token
|
# For unmanaged, if alias is present, replace master token with it
|
||||||
|
master_token = None if self.isManaged() else (initResult.alias_token or self._cfg.master_token)
|
||||||
self._cfg = self._cfg._replace(
|
self._cfg = self._cfg._replace(
|
||||||
master_token=master_token,
|
master_token=master_token,
|
||||||
own_token=initResult.own_token,
|
own_token=initResult.own_token,
|
||||||
@ -284,9 +285,10 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# On first successfull initialization request, master token will dissapear for managed hosts so it will be no more available (not needed anyway)
|
# On first successfull initialization request, master token will dissapear for managed hosts
|
||||||
if self.isManaged():
|
# so it will be no more available (not needed anyway). For unmanaged, the master token will
|
||||||
platform.store.writeConfig(self._cfg)
|
# be replaced with an alias token.
|
||||||
|
platform.store.writeConfig(self._cfg)
|
||||||
|
|
||||||
# Setup logger now
|
# Setup logger now
|
||||||
if self._cfg.own_token:
|
if self._cfg.own_token:
|
||||||
|
@ -50,6 +50,7 @@ class InitializationResultType(typing.NamedTuple):
|
|||||||
own_token: typing.Optional[str] = None
|
own_token: typing.Optional[str] = None
|
||||||
unique_id: typing.Optional[str] = None
|
unique_id: typing.Optional[str] = None
|
||||||
os: typing.Optional[ActorOsConfigurationType] = None
|
os: typing.Optional[ActorOsConfigurationType] = None
|
||||||
|
alias_token: typing.Optional[str] = None
|
||||||
|
|
||||||
class LoginResultInfoType(typing.NamedTuple):
|
class LoginResultInfoType(typing.NamedTuple):
|
||||||
ip: str
|
ip: str
|
||||||
|
@ -42,12 +42,13 @@ from uds.models import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# from uds.core import VERSION
|
# from uds.core import VERSION
|
||||||
from uds.core.managers import userServiceManager
|
from uds.core.managers import userServiceManager, cryptoManager
|
||||||
from uds.core import osmanagers
|
from uds.core import osmanagers
|
||||||
from uds.core.util import log, certs
|
from uds.core.util import log, certs
|
||||||
from uds.core.util.state import State
|
from uds.core.util.state import State
|
||||||
from uds.core.util.cache import Cache
|
from uds.core.util.cache import Cache
|
||||||
from uds.core.util.config import GlobalConfig
|
from uds.core.util.config import GlobalConfig
|
||||||
|
from uds.models.service import ServiceTokenAlias
|
||||||
|
|
||||||
from ..handlers import Handler, AccessDenied, RequestError
|
from ..handlers import Handler, AccessDenied, RequestError
|
||||||
|
|
||||||
@ -241,11 +242,23 @@ class Initialize(ActorV3Action):
|
|||||||
# First, validate token...
|
# First, validate token...
|
||||||
logger.debug('Args: %s, Params: %s', self._args, self._params)
|
logger.debug('Args: %s, Params: %s', self._args, self._params)
|
||||||
service: typing.Optional[Service] = None
|
service: typing.Optional[Service] = None
|
||||||
|
alias_token: typing.Optional[str] = None
|
||||||
try:
|
try:
|
||||||
|
token = self._params['token']
|
||||||
# First, try to locate an user service providing this token.
|
# First, try to locate an user service providing this token.
|
||||||
if self._params['type'] == UNMANAGED:
|
if self._params['type'] == UNMANAGED:
|
||||||
# If unmanaged, use Service locator
|
alias_token = token # Store token as possible alias
|
||||||
service = Service.objects.get(token=self._params['token'])
|
# First, try to locate on alias table
|
||||||
|
if ServiceTokenAlias.objects.get(alias=token).exists():
|
||||||
|
# Retrieve real service from token alias
|
||||||
|
service = ServiceTokenAlias.objects.get(alias=token).service
|
||||||
|
# If not found, try to locate on service table
|
||||||
|
if service is None: # Not on alias token, try to locate on Service table
|
||||||
|
service = Service.objects.get(token=token)
|
||||||
|
# And create a new alias for it, and save
|
||||||
|
alias_token = cryptoManager().randomString() # fix alias with new token
|
||||||
|
service.aliases.create(alias=alias_token)
|
||||||
|
|
||||||
# Locate an userService that belongs to this service and which
|
# Locate an userService that belongs to this service and which
|
||||||
# Build the possible ids and make initial filter to match service
|
# Build the possible ids and make initial filter to match service
|
||||||
idsList = [x['ip'] for x in self._params['id']] + [
|
idsList = [x['ip'] for x in self._params['id']] + [
|
||||||
@ -255,7 +268,7 @@ class Initialize(ActorV3Action):
|
|||||||
else:
|
else:
|
||||||
# If not service provided token, use actor tokens
|
# If not service provided token, use actor tokens
|
||||||
ActorToken.objects.get(
|
ActorToken.objects.get(
|
||||||
token=self._params['token']
|
token=token
|
||||||
) # Not assigned, because only needs check
|
) # Not assigned, because only needs check
|
||||||
# Build the possible ids and make initial filter to match ANY userservice with provided MAC
|
# Build the possible ids and make initial filter to match ANY userservice with provided MAC
|
||||||
idsList = [i['mac'] for i in self._params['id'][:5]]
|
idsList = [i['mac'] for i in self._params['id'][:5]]
|
||||||
@ -280,7 +293,7 @@ class Initialize(ActorV3Action):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.info('Unmanaged host request: %s, %s', self._params, e)
|
logger.info('Unmanaged host request: %s, %s', self._params, e)
|
||||||
return ActorV3Action.actorResult(
|
return ActorV3Action.actorResult(
|
||||||
{'own_token': None, 'max_idle': None, 'unique_id': None, 'os': None}
|
{'own_token': None, 'max_idle': None, 'unique_id': None, 'os': None, 'alias': None}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Managed by UDS, get initialization data from osmanager and return it
|
# Managed by UDS, get initialization data from osmanager and return it
|
||||||
@ -296,6 +309,9 @@ class Initialize(ActorV3Action):
|
|||||||
'own_token': userService.uuid,
|
'own_token': userService.uuid,
|
||||||
'unique_id': userService.unique_id,
|
'unique_id': userService.unique_id,
|
||||||
'os': osData,
|
'os': osData,
|
||||||
|
# alias will contain a new master token (or same alias if not a token) to allow change on unmanaged machines.
|
||||||
|
# Managed machines will not use this field (will return None)
|
||||||
|
'alias_token': alias_token,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
except (ActorToken.DoesNotExist, Service.DoesNotExist):
|
except (ActorToken.DoesNotExist, Service.DoesNotExist):
|
||||||
|
@ -5,8 +5,8 @@ import typing
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from uds import models
|
from uds import models
|
||||||
from uds.core.util.stats.events import EVENT_NAMES
|
|
||||||
from uds.core.util.cache import Cache
|
from uds.core.util.cache import Cache
|
||||||
|
from uds.core.util.stats import events, counters
|
||||||
|
|
||||||
from . import types
|
from . import types
|
||||||
|
|
||||||
@ -19,13 +19,14 @@ class StatInterval(typing.NamedTuple):
|
|||||||
end: datetime.datetime
|
end: datetime.datetime
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def start_poxix(self) -> int:
|
def start_timestamp(self) -> int:
|
||||||
return calendar.timegm(self.start.timetuple())
|
return calendar.timegm(self.start.timetuple())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def end_poxix(self) -> int:
|
def end_timestamp(self) -> int:
|
||||||
return calendar.timegm(self.end.timetuple())
|
return calendar.timegm(self.end.timetuple())
|
||||||
|
|
||||||
|
|
||||||
class VirtualFileInfo(typing.NamedTuple):
|
class VirtualFileInfo(typing.NamedTuple):
|
||||||
name: str
|
name: str
|
||||||
size: int
|
size: int
|
||||||
@ -34,6 +35,7 @@ class VirtualFileInfo(typing.NamedTuple):
|
|||||||
# Cache stamp
|
# Cache stamp
|
||||||
stamp: int = -1
|
stamp: int = -1
|
||||||
|
|
||||||
|
|
||||||
# Dispatcher needs an Interval, an extensio, the size and the offset
|
# Dispatcher needs an Interval, an extensio, the size and the offset
|
||||||
DispatcherType = typing.Callable[[StatInterval, str, int, int], bytes]
|
DispatcherType = typing.Callable[[StatInterval, str, int, int], bytes]
|
||||||
|
|
||||||
@ -77,6 +79,7 @@ class StatsFS(types.UDSFSInterface):
|
|||||||
self._dispatchers = {
|
self._dispatchers = {
|
||||||
'events': (self._read_events, True),
|
'events': (self._read_events, True),
|
||||||
'pools': (self._read_pools, False),
|
'pools': (self._read_pools, False),
|
||||||
|
'auths': (self._read_auths, False),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Splits the filename and returns a tuple with "dispatcher", "interval", "extension"
|
# Splits the filename and returns a tuple with "dispatcher", "interval", "extension"
|
||||||
@ -92,7 +95,12 @@ class StatsFS(types.UDSFSInterface):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raise FileNotFoundError
|
raise FileNotFoundError
|
||||||
|
|
||||||
logger.debug('Dispatcher: %s, interval: %s, extension: %s', dispatcher, interval, extension)
|
logger.debug(
|
||||||
|
'Dispatcher: %s, interval: %s, extension: %s',
|
||||||
|
dispatcher,
|
||||||
|
interval,
|
||||||
|
extension,
|
||||||
|
)
|
||||||
|
|
||||||
if dispatcher not in self._dispatchers:
|
if dispatcher not in self._dispatchers:
|
||||||
raise FileNotFoundError
|
raise FileNotFoundError
|
||||||
@ -101,14 +109,16 @@ class StatsFS(types.UDSFSInterface):
|
|||||||
|
|
||||||
if extension == '' and requiresInterval is True:
|
if extension == '' and requiresInterval is True:
|
||||||
raise FileNotFoundError
|
raise FileNotFoundError
|
||||||
|
|
||||||
if requiresInterval:
|
if requiresInterval:
|
||||||
if interval not in self._interval:
|
if interval not in self._interval:
|
||||||
raise FileNotFoundError
|
raise FileNotFoundError
|
||||||
|
|
||||||
range = self._interval[interval]
|
range = self._interval[interval]
|
||||||
else:
|
else:
|
||||||
range = (StatsFS._interval['lastmonth']) # Any value except "today" will do the trick
|
range = StatsFS._interval[
|
||||||
|
'lastmonth'
|
||||||
|
] # Any value except "today" will do the trick
|
||||||
extension = interval
|
extension = interval
|
||||||
|
|
||||||
if extension != 'csv':
|
if extension != 'csv':
|
||||||
@ -130,14 +140,22 @@ class StatsFS(types.UDSFSInterface):
|
|||||||
# If len(path) == 0, return the list of possible stats files (from _dispatchers)
|
# If len(path) == 0, return the list of possible stats files (from _dispatchers)
|
||||||
# else, raise an FileNotFoundError
|
# else, raise an FileNotFoundError
|
||||||
if len(path) == 0:
|
if len(path) == 0:
|
||||||
return ['.', '..'] + [
|
return (
|
||||||
f'{dispatcher}.{interval}.csv'
|
['.', '..']
|
||||||
for dispatcher in filter(lambda x: self._dispatchers[x][1], self._dispatchers)
|
+ [
|
||||||
for interval in self._interval
|
f'{dispatcher}.{interval}.csv'
|
||||||
] + [
|
for dispatcher in filter(
|
||||||
f'{dispatcher}.csv'
|
lambda x: self._dispatchers[x][1], self._dispatchers
|
||||||
for dispatcher in filter(lambda x: self._dispatchers[x][1] is False, self._dispatchers)
|
)
|
||||||
]
|
for interval in self._interval
|
||||||
|
]
|
||||||
|
+ [
|
||||||
|
f'{dispatcher}.csv'
|
||||||
|
for dispatcher in filter(
|
||||||
|
lambda x: self._dispatchers[x][1] is False, self._dispatchers
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
raise FileNotFoundError
|
raise FileNotFoundError
|
||||||
|
|
||||||
@ -156,31 +174,47 @@ class StatsFS(types.UDSFSInterface):
|
|||||||
cacheTime = 60
|
cacheTime = 60
|
||||||
|
|
||||||
# Check if the file info is cached
|
# Check if the file info is cached
|
||||||
cached = self._cache.get(path[0])
|
cached = self._cache.get(path[0]+extension)
|
||||||
if cached is not None:
|
if cached is not None:
|
||||||
logger.debug('Cache hit for %s', path[0])
|
logger.debug('Cache hit for %s', path[0])
|
||||||
return cached
|
data = cached
|
||||||
|
else:
|
||||||
|
logger.debug('Cache miss for %s', path[0])
|
||||||
|
data = dispatcher(interval, extension, 0, 0)
|
||||||
|
self._cache.put(path[0]+extension, data, cacheTime)
|
||||||
|
|
||||||
# Calculate the size of the file
|
# Calculate the size of the file
|
||||||
size = len(dispatcher(interval, extension, 0, 0))
|
size = len(data)
|
||||||
logger.debug('Size of %s: %s', path[0], size)
|
logger.debug('Size of %s: %s', path[0], size)
|
||||||
|
|
||||||
data = types.StatType(
|
return types.StatType(
|
||||||
st_mode=(stat.S_IFREG | 0o755),
|
st_mode=(stat.S_IFREG | 0o755),
|
||||||
st_nlink=1,
|
st_nlink=1,
|
||||||
st_size=size,
|
st_size=size,
|
||||||
st_mtime=interval.start_poxix,
|
st_mtime=interval.start_timestamp,
|
||||||
)
|
)
|
||||||
|
|
||||||
# store in cache
|
|
||||||
self._cache.put(path[0], data, cacheTime)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def read(self, path: typing.List[str], size: int, offset: int) -> bytes:
|
def read(self, path: typing.List[str], size: int, offset: int) -> bytes:
|
||||||
logger.debug('Reading data from %s: offset: %s, size: %s', path, offset, size)
|
logger.debug('Reading data from %s: offset: %s, size: %s', path, offset, size)
|
||||||
|
|
||||||
dispatcher, interval, extension = self.getFilenameComponents(path)
|
dispatcher, interval, extension = self.getFilenameComponents(path)
|
||||||
|
|
||||||
|
# if interval is today, cache time is 10 seconds, else cache time is 60 seconds
|
||||||
|
if interval == StatsFS._interval['today']:
|
||||||
|
cacheTime = 10
|
||||||
|
else:
|
||||||
|
cacheTime = 60
|
||||||
|
|
||||||
|
# Check if the file info is cached
|
||||||
|
cached = self._cache.get(path[0]+extension)
|
||||||
|
if cached is not None:
|
||||||
|
logger.debug('Cache hit for %s', path[0])
|
||||||
|
data = cached
|
||||||
|
else:
|
||||||
|
logger.debug('Cache miss for %s', path[0])
|
||||||
|
data = dispatcher(interval, extension, 0, 0)
|
||||||
|
self._cache.put(path[0]+extension, data, cacheTime)
|
||||||
|
|
||||||
# Dispatch the read to the dispatcher
|
# Dispatch the read to the dispatcher
|
||||||
data = dispatcher(interval, extension, size, offset)
|
data = dispatcher(interval, extension, size, offset)
|
||||||
logger.debug('Readed %s data length', len(data))
|
logger.debug('Readed %s data length', len(data))
|
||||||
@ -201,7 +235,7 @@ class StatsFS(types.UDSFSInterface):
|
|||||||
virtualFile = models.StatsEvents.getCSVHeader().encode() + b'\n'
|
virtualFile = models.StatsEvents.getCSVHeader().encode() + b'\n'
|
||||||
# stamp is unix timestamp
|
# stamp is unix timestamp
|
||||||
for record in models.StatsEvents.objects.filter(
|
for record in models.StatsEvents.objects.filter(
|
||||||
stamp__gte=interval.start_poxix, stamp__lte=interval.end_poxix
|
stamp__gte=interval.start_timestamp, stamp__lte=interval.end_timestamp
|
||||||
):
|
):
|
||||||
virtualFile += record.toCsv().encode() + b'\n'
|
virtualFile += record.toCsv().encode() + b'\n'
|
||||||
|
|
||||||
@ -210,10 +244,33 @@ class StatsFS(types.UDSFSInterface):
|
|||||||
def _read_pools(
|
def _read_pools(
|
||||||
self, interval: StatInterval, extension: str, size: int, offset: int
|
self, interval: StatInterval, extension: str, size: int, offset: int
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
logger.debug('Reading pools. Interval=%s, extension=%s, offset: %s, size: %s', interval, extension, offset, size)
|
logger.debug(
|
||||||
|
'Reading pools. Interval=%s, extension=%s, offset: %s, size: %s',
|
||||||
|
interval,
|
||||||
|
extension,
|
||||||
|
offset,
|
||||||
|
size,
|
||||||
|
)
|
||||||
# Compose the csv file from what we now of service pools
|
# Compose the csv file from what we now of service pools
|
||||||
virtualFile = models.ServicePool.getCSVHeader().encode() + b'\n'
|
virtualFile = models.ServicePool.getCSVHeader().encode() + b'\n'
|
||||||
# First, get the list of service pools
|
# First, get the list of service pools
|
||||||
for pool in models.ServicePool.objects.all().order_by('name'):
|
for pool in models.ServicePool.objects.all().order_by('name'):
|
||||||
virtualFile += pool.toCsv().encode() + b'\n'
|
virtualFile += pool.toCsv().encode() + b'\n'
|
||||||
return virtualFile
|
return virtualFile
|
||||||
|
|
||||||
|
def _read_auths(
|
||||||
|
self, interval: StatInterval, extension: str, size: int, offset: int
|
||||||
|
) -> bytes:
|
||||||
|
logger.debug(
|
||||||
|
'Reading auths. Interval=%s, extension=%s, offset: %s, size: %s',
|
||||||
|
interval,
|
||||||
|
extension,
|
||||||
|
offset,
|
||||||
|
size,
|
||||||
|
)
|
||||||
|
# Compose the csv file from what we now of service pools
|
||||||
|
virtualFile = models.Authenticator.getCSVHeader().encode() + b'\n'
|
||||||
|
# First, get the list of service pools
|
||||||
|
for auth in models.Authenticator.objects.all().order_by('name'):
|
||||||
|
virtualFile += auth.toCsv().encode() + b'\n'
|
||||||
|
return virtualFile
|
||||||
|
22
server/src/uds/migrations/0044_servicetokenalias.py
Normal file
22
server/src/uds/migrations/0044_servicetokenalias.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-11-11 13:16
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('uds', '0043_clean_unused_config'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ServiceTokenAlias',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('alias', models.CharField(max_length=128)),
|
||||||
|
('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aliases', to='uds.service')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
@ -43,7 +43,7 @@ from .permissions import Permissions
|
|||||||
|
|
||||||
# Services
|
# Services
|
||||||
from .provider import Provider
|
from .provider import Provider
|
||||||
from .service import Service
|
from .service import Service, ServiceTokenAlias
|
||||||
|
|
||||||
# Os managers
|
# Os managers
|
||||||
from .os_manager import OSManager
|
from .os_manager import OSManager
|
||||||
|
@ -245,6 +245,31 @@ class Authenticator(ManagedObjectModel, TaggingMixin):
|
|||||||
# Clears related permissions
|
# Clears related permissions
|
||||||
clean(toDelete)
|
clean(toDelete)
|
||||||
|
|
||||||
|
|
||||||
|
# returns CSV header
|
||||||
|
@staticmethod
|
||||||
|
def getCSVHeader(sep: str = ',') -> str:
|
||||||
|
return sep.join(
|
||||||
|
[
|
||||||
|
'name',
|
||||||
|
'type',
|
||||||
|
'users',
|
||||||
|
'groups',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return record as csv line using separator (default: ',')
|
||||||
|
def toCsv(self, sep: str = ',') -> str:
|
||||||
|
return sep.join(
|
||||||
|
[
|
||||||
|
self.name,
|
||||||
|
self.data_type,
|
||||||
|
str(self.users.count()),
|
||||||
|
str(self.groups.count()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return u"{0} of type {1} (id:{2})".format(self.name, self.data_type, self.id)
|
return u"{0} of type {1} (id:{2})".format(self.name, self.data_type, self.id)
|
||||||
|
|
||||||
|
@ -54,6 +54,20 @@ if typing.TYPE_CHECKING:
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceTokenAlias(models.Model):
|
||||||
|
"""
|
||||||
|
This model stores the alias for a service token.
|
||||||
|
"""
|
||||||
|
|
||||||
|
service = models.ForeignKey(
|
||||||
|
'Service', on_delete=models.CASCADE, related_name='aliases'
|
||||||
|
)
|
||||||
|
alias = models.CharField(max_length=128)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.alias
|
||||||
|
|
||||||
|
|
||||||
class Service(ManagedObjectModel, TaggingMixin): # type: ignore
|
class Service(ManagedObjectModel, TaggingMixin): # type: ignore
|
||||||
"""
|
"""
|
||||||
A Service represents an specidied type of service offered to final users,
|
A Service represents an specidied type of service offered to final users,
|
||||||
@ -79,6 +93,8 @@ class Service(ManagedObjectModel, TaggingMixin): # type: ignore
|
|||||||
# "fake" declarations for type checking
|
# "fake" declarations for type checking
|
||||||
objects: 'models.BaseManager[Service]'
|
objects: 'models.BaseManager[Service]'
|
||||||
deployedServices: 'models.QuerySet[ServicePool]'
|
deployedServices: 'models.QuerySet[ServicePool]'
|
||||||
|
aliases: 'models.QuerySet[ServiceTokenAlias]'
|
||||||
|
|
||||||
|
|
||||||
class Meta(ManagedObjectModel.Meta):
|
class Meta(ManagedObjectModel.Meta):
|
||||||
"""
|
"""
|
||||||
|
@ -78,6 +78,7 @@ class AuthenticatorsStats(StatsReportAuto):
|
|||||||
typing.cast('models.Authenticator', a),
|
typing.cast('models.Authenticator', a),
|
||||||
counters.CT_AUTH_SERVICES,
|
counters.CT_AUTH_SERVICES,
|
||||||
since=since,
|
since=since,
|
||||||
|
to=to,
|
||||||
interval=interval,
|
interval=interval,
|
||||||
limit=MAX_ELEMENTS,
|
limit=MAX_ELEMENTS,
|
||||||
use_max=True,
|
use_max=True,
|
||||||
@ -88,6 +89,7 @@ class AuthenticatorsStats(StatsReportAuto):
|
|||||||
typing.cast('models.Authenticator', a),
|
typing.cast('models.Authenticator', a),
|
||||||
counters.CT_AUTH_USERS_WITH_SERVICES,
|
counters.CT_AUTH_USERS_WITH_SERVICES,
|
||||||
since=since,
|
since=since,
|
||||||
|
to=to,
|
||||||
interval=interval,
|
interval=interval,
|
||||||
limit=MAX_ELEMENTS,
|
limit=MAX_ELEMENTS,
|
||||||
use_max=True,
|
use_max=True,
|
||||||
@ -97,6 +99,7 @@ class AuthenticatorsStats(StatsReportAuto):
|
|||||||
typing.cast('models.Authenticator', a),
|
typing.cast('models.Authenticator', a),
|
||||||
counters.CT_AUTH_USERS,
|
counters.CT_AUTH_USERS,
|
||||||
since=since,
|
since=since,
|
||||||
|
to=to,
|
||||||
interval=interval,
|
interval=interval,
|
||||||
limit=MAX_ELEMENTS,
|
limit=MAX_ELEMENTS,
|
||||||
use_max=True,
|
use_max=True,
|
||||||
|
Loading…
Reference in New Issue
Block a user