Skip to content

Commit 2734d73

Browse files
authored
Feature/add audit api (#40)
* Adding support for the Audit API.
1 parent d38f034 commit 2734d73

File tree

8 files changed

+416
-1
lines changed

8 files changed

+416
-1
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# Changelog
22

3+
* Adding support for the Audit API.
4+
35
* Added support for event attachment handling.
46

57
* Adding support for bulk operations.
68

79
## Version 1.6.1
810

9-
* Adding `c8y_tk` namespace to distribution.
11+
* Adding `c8y_tk` namespace to distribution.
1012

1113
## Version 1.6
1214

c8y_api/_base_api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class CumulocityRestApi:
2929
ACCEPT_MANAGED_OBJECT = 'application/vnd.com.nsn.cumulocity.managedobject+json'
3030
ACCEPT_USER = 'application/vnd.com.nsn.cumulocity.user+json'
3131
ACCEPT_GLOBAL_ROLE = 'application/vnd.com.nsn.cumulocity.group+json'
32+
CONTENT_AUDIT_RECORD = 'application/vnd.com.nsn.cumulocity.auditrecord+json'
3233
CONTENT_MEASUREMENT_COLLECTION = 'application/vnd.com.nsn.cumulocity.measurementcollection+json'
3334

3435
def __init__(self, base_url: str, tenant_id: str, username: str = None, password: str = None, tfa_token: str = None,

c8y_api/_main_api.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from c8y_api.model.notification2 import Subscriptions, Tokens
2020
from c8y_api.model.operations import Operations, BulkOperations
2121
from c8y_api.model.tenant_options import TenantOptions
22+
from c8y_api.model.audit import AuditRecords
2223

2324

2425
class CumulocityApi(CumulocityRestApi):
@@ -48,6 +49,7 @@ def __init__(self, base_url: str, tenant_id: str, username: str = None, password
4849
self.__tenant_options = TenantOptions(self)
4950
self.__notification2_subscriptions = Subscriptions(self)
5051
self.__notification2_tokens = Tokens(self)
52+
self.__audit_records = AuditRecords(self)
5153

5254
@property
5355
def measurements(self) -> Measurements:
@@ -138,3 +140,8 @@ def notification2_subscriptions(self) -> Subscriptions:
138140
def notification2_tokens(self) -> Tokens:
139141
"""Provide access to the Notification 2.0 Tokens API."""
140142
return self.__notification2_tokens
143+
144+
@property
145+
def audit_records(self) -> AuditRecords:
146+
"""Provide access to the Audit API."""
147+
return self.__audit_records

c8y_api/model/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from c8y_api.model.administration import *
88
from c8y_api.model.applications import *
99
from c8y_api.model.alarms import *
10+
from c8y_api.model.audit import *
1011
from c8y_api.model.binaries import *
1112
from c8y_api.model.events import *
1213
from c8y_api.model.identity import *
@@ -20,6 +21,7 @@
2021
'User', 'GlobalRole', 'InventoryRole', 'Users', 'GlobalRoles', 'InventoryRoles', 'InventoryRoleAssignment',
2122
'Permission', 'ReadPermission', 'WritePermission', 'AnyPermission',
2223
'Application',
24+
'AuditRecord', 'AuditRecords',
2325
'Operation', 'BulkOperation', 'Operations', 'BulkOperations',
2426
'ManagedObject', 'Device', 'DeviceGroup', 'Fragment', 'NamedObject',
2527
'Inventory', 'DeviceInventory', 'DeviceGroupInventory',

c8y_api/model/audit.py

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# Copyright (c) 2021 Software AG,
2+
# Darmstadt, Germany and/or Software AG USA Inc., Reston, VA, USA,
3+
# and/or its subsidiaries and/or its affiliates and/or their licensors.
4+
# Use, reproduction, transfer, publication or disclosure is prohibited except
5+
# as specifically provided for in your License Agreement with Software AG.
6+
7+
from __future__ import annotations
8+
9+
from datetime import datetime, timedelta
10+
from typing import Generator, List
11+
12+
from c8y_api._base_api import CumulocityRestApi
13+
from c8y_api.model._base import CumulocityResource, ComplexObject
14+
from c8y_api.model._parser import ComplexObjectParser
15+
from c8y_api.model._util import _DateUtil
16+
17+
18+
class AuditRecord(ComplexObject):
19+
"""Represents an Audit Record object within Cumulocity.
20+
21+
Instances of this class are returned by functions of the corresponding
22+
Audits API. Use this class to create new or update AuditRecord objects.
23+
24+
See also: https://cumulocity.com/api/core/#tag/Audits
25+
"""
26+
27+
class Severity:
28+
"""Audit severity levels."""
29+
MAJOR = 'MAJOR'
30+
CRITICAL = 'CRITICAL'
31+
MINOR = 'MINOR'
32+
WARNING = 'WARNING'
33+
INFORMATION = 'information' # for whatever reason, this is used.
34+
35+
_parser = ComplexObjectParser({
36+
'type': 'type',
37+
'time': 'time',
38+
'creation_time': 'creationTime',
39+
'activity': 'activity',
40+
'text': 'text',
41+
'severity': 'severity',
42+
'user': 'user',
43+
'application': 'application'}, [] )
44+
_resource = '/audit/auditRecords'
45+
_accept = CumulocityRestApi.CONTENT_AUDIT_RECORD
46+
47+
def __init__(self, c8y: CumulocityRestApi = None, type: str = None, time: str | datetime = None, # noqa (type)
48+
source: str = None, activity: str = None, text: str = None, severity: str = None,
49+
application: str = None, user: str = None, **kwargs):
50+
"""Create a new AuditRecord object.
51+
52+
Args:
53+
c8y (CumulocityRestApi): Cumulocity connection reference; needs
54+
to be set for direct manipulation (create, delete)
55+
type (str): Audit records type
56+
time (str|datetime): Date/time of the audit records Can be
57+
provided as timezone-aware datetime object or formatted
58+
string (in standard ISO format incl. timezone:
59+
YYYY-MM-DD'T'HH:MM:SS.SSSZ as it is returned by the
60+
Cumulocity REST API).
61+
Use 'now' to set to current datetime in UTC.
62+
source (str): The managed object ID to which the audit is associated
63+
activity (str): Summary of the action that was carried out
64+
text (str): Details of the action that was carried out
65+
severity (str): Severity of the audit record.
66+
application (str): The application from which the record was created.
67+
user (str): The user who carried out the activity
68+
kwargs: Additional arguments are treated as custom fragments
69+
"""
70+
super().__init__(c8y, **kwargs)
71+
self.type = type
72+
self.time = _DateUtil.ensure_timestring(time)
73+
self.creation_time = None # undocumented property
74+
self.source = source
75+
self.activity = activity
76+
self.text = text
77+
self.severity = severity # undocumented property
78+
self.application = application
79+
self.user = user
80+
81+
@property
82+
def datetime(self) -> datetime:
83+
"""Convert the audit record's time to a Python datetime object.
84+
85+
Returns:
86+
Standard Python datetime object
87+
"""
88+
return super()._to_datetime(self.time)
89+
90+
@property
91+
def creation_datetime(self) -> datetime:
92+
"""Convert the audit record's creation time to a Python
93+
datetime object.
94+
95+
Returns:
96+
Standard Python datetime object
97+
"""
98+
return super()._to_datetime(self.creation_time)
99+
100+
@classmethod
101+
def from_json(cls, json: dict) -> AuditRecord:
102+
# (no doc update required)
103+
obj = super()._from_json(json, AuditRecord())
104+
obj.source = json['source']['id']
105+
return obj
106+
107+
def to_json(self, only_updated: bool = False) -> dict:
108+
# (no doc update required)
109+
obj_json = super()._to_json(only_updated, exclude={'creation_time'})
110+
# source needs to be set manually, but it cannot be updated
111+
if not only_updated and self.source:
112+
obj_json['source'] = {'id': self.source}
113+
return obj_json
114+
115+
def create(self) -> AuditRecord:
116+
"""Create the AuditRecord within the database.
117+
118+
Returns:
119+
A fresh AuditRecord object representing what was
120+
created within the database (including the ID).
121+
"""
122+
return super()._create()
123+
124+
125+
class AuditRecords(CumulocityResource):
126+
"""Provides access to the Audit API.
127+
128+
This class can be used for get, search for, create, update and
129+
delete records within the Cumulocity database.
130+
131+
See also: https://cumulocity.com/api/core/#tag/Audits
132+
"""
133+
134+
def __init__(self, c8y):
135+
super().__init__(c8y, '/audit/auditRecords')
136+
137+
def get(self, record_id: str) -> AuditRecord:
138+
"""Retrieve a specific object from the database.
139+
140+
Args:
141+
record_id (str): The database ID of the audit record
142+
143+
Returns:
144+
An AuditRecord instance representing the object in the database.
145+
"""
146+
audit_obj = AuditRecord.from_json(self._get_object(record_id))
147+
audit_obj.c8y = self.c8y # inject c8y connection into instance
148+
return audit_obj
149+
150+
def select(self, type: str = None, source: str = None, application: str = None, user: str = None, # noqa (type)
151+
before: str | datetime = None, after: str | datetime = None,
152+
min_age: timedelta = None, max_age: timedelta = None,
153+
reverse: bool = False, limit: int = None, page_size: int = 1000) -> Generator[AuditRecord]:
154+
"""Query the database for audit records and iterate over the results.
155+
156+
This function is implemented in a lazy fashion - results will only be
157+
fetched from the database as long there is a consumer for them.
158+
159+
All parameters are considered to be filters, limiting the result set
160+
to objects which meet the filters' specification. Filters can be
161+
combined (within reason).
162+
163+
Args:
164+
type (str): Audit record type
165+
source (str): Database ID of a source device
166+
application (str): Application from which the audit was carried out.
167+
user (str): The user who carried out the activity.
168+
before (str|datetime): Datetime object or ISO date/time string. Only
169+
records assigned to a time before this date are returned.
170+
after (str|datetime): Datetime object or ISO date/time string. Only
171+
records assigned to a time after this date are returned.
172+
min_age (timedelta): Minimum age for selected records.
173+
max_age (timedelta): Maximum age for selected records.
174+
reverse (bool): Invert the order of results, starting with the
175+
most recent one.
176+
limit (int): Limit the number of results to this number.
177+
page_size (int): Define the number of objects which are read (and
178+
parsed in one chunk). This is a performance related setting.
179+
180+
Returns:
181+
Generator for AuditRecord objects
182+
"""
183+
base_query = self._build_base_query(type=type, source=source, application=application, user=user,
184+
before=before, after=after,
185+
min_age=min_age, max_age=max_age,
186+
reverse=reverse, page_size=page_size)
187+
return super()._iterate(base_query, limit, AuditRecord.from_json)
188+
189+
def get_all(self, type: str = None, source: str = None, application: str = None, user: str = None, # noqa (type)
190+
before: str | datetime = None, after: str | datetime = None,
191+
min_age: timedelta = None, max_age: timedelta = None,
192+
reverse: bool = False, limit: int = None, page_size: int = 1000) -> List[AuditRecord]:
193+
"""Query the database for audit records and return the results as list.
194+
195+
This function is a greedy version of the `select` function. All
196+
available results are read immediately and returned as list.
197+
198+
See `select` for a documentation of arguments.
199+
200+
Returns:
201+
List of AuditRecord objects
202+
"""
203+
return list(self.select(type=type, source=source, application=application, user=user,
204+
before=before, after=after,
205+
min_age=min_age, max_age=max_age,
206+
reverse=reverse, limit=limit, page_size=page_size))
207+
208+
def create(self, *records: AuditRecord):
209+
"""Create audit record objects within the database.
210+
211+
Note: If not yet defined, this will set the record date to now in
212+
each of the given objects.
213+
214+
Args:
215+
records (*AuditRecord): Collection of AuditRecord instances
216+
"""
217+
for r in records:
218+
if not r.time:
219+
r.time = _DateUtil.to_timestring(datetime.utcnow())
220+
super()._create(AuditRecord.to_full_json, *records)

integration_tests/test_audits.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Copyright (c) 2020 Software AG,
2+
# Darmstadt, Germany and/or Software AG USA Inc., Reston, VA, USA,
3+
# and/or its subsidiaries and/or its affiliates and/or their licensors.
4+
# Use, reproduction, transfer, publication or disclosure is prohibited except
5+
# as specifically provided for in your License Agreement with Software AG.
6+
7+
from datetime import timedelta
8+
9+
from c8y_api import CumulocityApi
10+
from c8y_api.model import AuditRecord
11+
from c8y_api.model._util import _DateUtil # noqa
12+
13+
from tests import RandomNameGenerator
14+
15+
16+
def test_CR(live_c8y: CumulocityApi, sample_device): # noqa (case)
17+
"""Verify that basic creation, lookup and update of Audit Records
18+
works as expected."""
19+
20+
name = RandomNameGenerator.random_name()
21+
22+
# (1) create audit record
23+
before = _DateUtil.now()
24+
record = AuditRecord(live_c8y, type=f'{name}_type', source=sample_device.id, time='now',
25+
severity=AuditRecord.Severity.INFORMATION,
26+
activity=f'{name} activity', text=f'detailed {name} text',
27+
application=f'{name}_app', user=live_c8y.username).create()
28+
after = _DateUtil.now()
29+
30+
# -> there should be exactly one audit record with that source
31+
records = live_c8y.audit_records.get_all(source=sample_device.id)
32+
assert len(records) == 1
33+
assert records[0].id == record.id
34+
35+
# -> there should be exactly one audit record with that application/user
36+
records = live_c8y.audit_records.get_all(application=record.application,
37+
user=record.user)
38+
assert len(records) == 1
39+
assert records[0].id == record.id
40+
41+
# -> there should be at least one audit record within that timeframe
42+
records = live_c8y.audit_records.get_all(before=after, after=before)
43+
assert len(records) >= 1
44+
assert record.id in [r.id for r in records]
45+
46+
# -> there should be at least one audit record within the last 5 seconds
47+
records = live_c8y.audit_records.get_all(min_age=timedelta(microseconds=0.1),
48+
max_age=timedelta(seconds=5.0))
49+
assert len(records) >= 1
50+
assert record.id in [r.id for r in records]

tests/model/audit_records.json

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"auditRecords": [
3+
{
4+
"activity": "Alarm created",
5+
"application": "apama-ctrl-1c-4g",
6+
"com_cumulocity_model_event_AuditSourceDevice": {
7+
"id": "18924"
8+
},
9+
"creationTime": "2020-11-19T10:37:06.737Z",
10+
"id": "20496939",
11+
"self": "https://t12345.cumulocity.com/audit/auditRecords/20496939",
12+
"severity": "CRITICAL",
13+
"source": {
14+
"id": "20496938",
15+
"self": "https://t12345.cumulocity.com/inventory/managedObjects/20496938"
16+
},
17+
"text": "Device name: 'Motor #1', alarm text: 'Production Failure'",
18+
"time": "1970-01-01T00:00:00.000Z",
19+
"type": "Alarm",
20+
"user": "service_apama-ctrl-1c-4g"
21+
},
22+
{
23+
"activity": "Alarm updated",
24+
"application": "devicemanagement",
25+
"changes": [
26+
{
27+
"attribute": "status",
28+
"newValue": "CLEARED",
29+
"previousValue": "ACTIVE",
30+
"type": "com.cumulocity.model.event.CumulocityAlarmStatuses"
31+
}
32+
],
33+
"com_cumulocity_model_event_AuditSourceDevice": {
34+
"id": "18924"
35+
},
36+
"creationTime": "2020-11-19T10:37:05.797Z",
37+
"id": "20496863",
38+
"self": "https://t12345.cumulocity.com/audit/auditRecords/20496863",
39+
"severity": "CRITICAL",
40+
"source": {
41+
"id": "20496059",
42+
"self": "https://t12345.cumulocity.com/inventory/managedObjects/20496059"
43+
},
44+
"text": "Device name: 'Motor #1', alarm text: 'Production Failure'",
45+
"time": "1970-01-01T00:00:00.000Z",
46+
"type": "Alarm",
47+
"user": "someone@softwareag.com"
48+
}
49+
],
50+
"next": "https://t12345.cumulocity.com/audit/auditRecords?application=apama-ctrl-1c-4g&pageSize=10&currentPage=2",
51+
"self": "https://t12345.cumulocity.com/audit/auditRecords?application=apama-ctrl-1c-4g&pageSize=10&currentPage=1",
52+
"statistics": {
53+
"currentPage": 1,
54+
"pageSize": 10
55+
}
56+
}

0 commit comments

Comments
 (0)