Skip to content

Commit 5f0a16c

Browse files
authored
Audit Logs for Postgres connections opened through a data link (#9873)
- Closes #9599 - Implemented API for sending audit logs to the cloud on a background thread. - If the Postgres connection is opened through a datalink, its internal JDBC connection is replaced by a wrapper that reports executed queries to the audit log. - Also introduces `EnsoMeta` - a helper Java class that can be used in our helper libraries to access Enso types. - I have replaced the common pattern scattered throughout the codebase with calls to this 'library' to avoid repetitive code. - Refactored `Table.display` to share code between in-memory and DB - it was needed as the function stopped working for `DB_Table` after adding making the `Table` constructor `private`. - Clearer error when reading a SQLite database from a remote file (tells the user to download it first). - Follow up - correlate asset id of the data link: #9869 - Follow up - include project name (once bug is fixed): #9875 - Some problems/improvements of the audit log: - The audit log system is not yet ready for high throughput of logs #9870 - The logs may be lost if `System.exit` is used #9871
1 parent 1d61c08 commit 5f0a16c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2175
-219
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,8 @@
661661
- [Added `Vector.build_multiple`, and better for support for errors and warnings
662662
inside `Vector.build` and `Vector.build_multiple`.][9766]
663663
- [Added `Vector.duplicates`.][9917]
664+
- [Log operations performed on a Postgres database connection obtained through a
665+
Data Link.][9873]
664666

665667
[debug-shortcuts]:
666668
https://github.com/enso-org/enso/blob/develop/app/gui/docs/product/shortcuts.md#debug
@@ -970,6 +972,7 @@
970972
[9750]: https://github.com/enso-org/enso/pull/9750
971973
[9766]: https://github.com/enso-org/enso/pull/9766
972974
[9917]: https://github.com/enso-org/enso/pull/9917
975+
[9873]: https://github.com/enso-org/enso/pull/9873
973976

974977
#### Enso Compiler
975978

distribution/lib/Standard/Base/0.0.0-dev/src/Enso_Cloud/Enso_User.enso

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import project.Data.Time.Duration.Duration
44
import project.Data.Vector.Vector
55
import project.Enso_Cloud.Enso_File.Enso_Asset_Type
66
import project.Enso_Cloud.Enso_File.Enso_File
7+
import project.Enso_Cloud.Errors.Not_Logged_In
8+
import project.Enso_Cloud.Internal.Authentication
79
import project.Enso_Cloud.Internal.Utils
810
import project.Error.Error
911
import project.Errors.Illegal_Argument.Illegal_Argument
1012
import project.Network.HTTP.HTTP
1113
import project.Network.HTTP.HTTP_Method.HTTP_Method
1214
import project.Nothing.Nothing
15+
import project.Panic.Panic
1316
from project.Data.Boolean import Boolean, False, True
1417
from project.Enso_Cloud.Public_Utils import get_optional_field, get_required_field
1518

@@ -29,12 +32,17 @@ type Enso_User
2932

3033
## ICON people
3134
Fetch the current user.
32-
current : Enso_User
33-
current =
35+
current -> Enso_User =
3436
Utils.get_cached "users/me" cache_duration=(Duration.new minutes=120) <|
3537
json = Utils.http_request_as_json HTTP_Method.Get (Utils.cloud_root_uri + "users/me")
3638
Enso_User.from json
3739

40+
## PRIVATE
41+
Checks if the user is logged in.
42+
is_logged_in -> Boolean =
43+
Panic.catch Not_Logged_In handler=(_->False) <|
44+
Authentication.get_access_token.is_error.not
45+
3846
## ICON people
3947
Lists all known users.
4048
list : Vector Enso_User
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import project.Data.Json.JS_Object
2+
import project.Data.Text.Text
3+
import project.Errors.Illegal_Argument.Illegal_Argument
4+
import project.Nothing.Nothing
5+
from project.Data.Boolean import Boolean, False, True
6+
7+
polyglot java import org.enso.base.enso_cloud.audit.AuditLog
8+
9+
## PRIVATE
10+
type Audit_Log
11+
## PRIVATE
12+
Reports an event to the audit log.
13+
The event is submitted asynchronously.
14+
15+
Arguments:
16+
- event_type: The type of the event.
17+
- message: The message associated with the event.
18+
- metadata: Additional metadata to include with the event.
19+
Note that it should be a JS object and it should _not_ contain fields
20+
that are restricted. These fields are added to the metadata
21+
automatically.
22+
- async: Whether to submit the event asynchronously.
23+
Defaults to True.
24+
25+
? Restricted Fields
26+
27+
The following fields are added by the system and should not be included
28+
in the provided metadata:
29+
- `type`
30+
- `operation`
31+
- `localTimestamp`
32+
- `projectName`
33+
report_event event_type:Text message:Text (metadata:JS_Object = JS_Object.from_pairs []) (async : Boolean = True) -> Nothing =
34+
Illegal_Argument.handle_java_exception <|
35+
case async of
36+
True -> AuditLog.logAsync event_type message metadata.object_node
37+
False -> AuditLog.logSynchronously event_type message metadata.object_node

distribution/lib/Standard/Base/0.0.0-dev/src/Enso_Cloud/Public_Utils.enso

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import project.Any.Any
22
import project.Data.Json.JS_Object
33
import project.Data.Text.Text
44
import project.Enso_Cloud.Errors.Enso_Cloud_Error
5+
import project.Enso_Cloud.Internal.Utils
56
import project.Error.Error
67
import project.Meta
78
import project.Nothing.Nothing
@@ -57,3 +58,11 @@ get_optional_field (key : Text) js_object (~if_missing = Nothing) (show_value :
5758
_ ->
5859
representation = if show_value then js_object.to_display_text else Meta.type_of js_object . to_display_text
5960
Error.throw (Enso_Cloud_Error.Invalid_Response_Payload "Expected a JSON object, but got "+representation+".")
61+
62+
## PRIVATE
63+
UNSTABLE
64+
Re-exports parts of the functionality of `http_request_as_json` function that
65+
is needed in tests.
66+
It should not be used anywhere else and may be removed in the near future.
67+
cloud_http_request_for_test method url_suffix =
68+
Utils.http_request_as_json method Utils.cloud_root_uri+url_suffix

distribution/lib/Standard/Base/0.0.0-dev/src/System/File_Format.enso

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ type File_Format
9696
_ = [file, on_problems]
9797
Unimplemented.throw "This is an interface only."
9898

99+
## PRIVATE
100+
Implements decoding the format from a stream.
101+
read_stream : Input_Stream -> File_Format_Metadata -> Any
102+
read_stream self stream:Input_Stream (metadata : File_Format_Metadata) =
103+
_ = [stream, metadata]
104+
Unimplemented.throw "This is an interface only."
105+
99106
## PRIVATE
100107
default_widget : Widget
101108
default_widget =

distribution/lib/Standard/Database/0.0.0-dev/src/Connection/Data_Link/Postgres_Data_Link.enso

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ type Postgres_Data_Link
4545
read self (format = Auto_Detect) (on_problems : Problem_Behavior) =
4646
_ = on_problems
4747
if format != Auto_Detect then Error.throw (Illegal_Argument.Error "Only the default Auto_Detect format should be used with a Postgres Data Link, because it does not point to a file resource, but a database entity, so setting a file format for it is meaningless.") else
48-
default_options = Connection_Options.Value
48+
# TODO add related asset id here: https://github.com/enso-org/enso/issues/9869
49+
audit_mode = if Enso_User.is_logged_in then "cloud" else "local"
50+
default_options = Connection_Options.Value [["enso.internal.audit", audit_mode]]
4951
connection = self.details.connect default_options
5052
case self of
5153
Postgres_Data_Link.Connection _ -> connection

distribution/lib/Standard/Database/0.0.0-dev/src/Connection/SQLite_Format.enso

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ from Standard.Base import all
22
import Standard.Base.Errors.Illegal_Argument.Illegal_Argument
33
import Standard.Base.System.File.Generic.Writable_File.Writable_File
44
import Standard.Base.System.File_Format_Metadata.File_Format_Metadata
5+
import Standard.Base.System.Input_Stream.Input_Stream
56
from Standard.Base.Metadata.Choice import Option
67

78
import project.Connection.Database
@@ -48,6 +49,12 @@ type SQLite_Format
4849
_ = [on_problems]
4950
Database.connect (SQLite.From_File file)
5051

52+
## PRIVATE
53+
read_stream : Input_Stream -> File_Format_Metadata -> Any
54+
read_stream self stream metadata =
55+
_ = [stream, metadata]
56+
Error.throw (Illegal_Argument.Error "Cannot connect to a SQLite database backed by a stream. Save it to a local file first.")
57+
5158
## PRIVATE
5259
Based on the File Format definition at: https://www.sqlite.org/fileformat.html
5360
magic_header_string =

distribution/lib/Standard/Database/0.0.0-dev/src/DB_Table.enso

Lines changed: 7 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import Standard.Table.Internal.Add_Row_Number
2323
import Standard.Table.Internal.Aggregate_Column_Helper
2424
import Standard.Table.Internal.Column_Naming_Helper.Column_Naming_Helper
2525
import Standard.Table.Internal.Constant_Column.Constant_Column
26+
import Standard.Table.Internal.Display_Helpers
2627
import Standard.Table.Internal.Join_Kind_Cross.Join_Kind_Cross
2728
import Standard.Table.Internal.Problem_Builder.Problem_Builder
2829
import Standard.Table.Internal.Replace_Helpers
@@ -35,10 +36,8 @@ import Standard.Table.Internal.Widget_Helpers
3536
import Standard.Table.Match_Columns as Match_Columns_Helpers
3637
import Standard.Table.Row.Row
3738
from Standard.Table import Aggregate_Column, Auto, Blank_Selector, Column_Ref, Data_Formatter, Join_Condition, Join_Kind, Match_Columns, Position, Previous_Value, Report_Unmatched, Set_Mode, Simple_Expression, Sort_Column, Table, Value_Type
38-
from Standard.Table.Column import get_item_string, normalize_string_for_display
3939
from Standard.Table.Errors import all
4040
from Standard.Table.Internal.Filter_Condition_Helpers import make_filter_column
41-
from Standard.Table.Table import print_table
4241

4342
import project.Connection.Connection.Connection
4443
import project.DB_Column.DB_Column
@@ -97,9 +96,12 @@ type DB_Table
9796
- format_terminal: whether ANSI-terminal formatting should be used
9897
display : Integer -> Boolean -> Text
9998
display self show_rows=10 format_terminal=False =
100-
df = self.read max_rows=show_rows warn_if_more_rows=False
101-
all_rows_count = self.row_count
102-
display_dataframe df indices_count=0 all_rows_count format_terminal
99+
data_fragment_with_warning = self.read max_rows=show_rows warn_if_more_rows=True
100+
has_more_rows = data_fragment_with_warning.has_warnings warning_type=Not_All_Rows_Downloaded
101+
data_fragment_cleared = data_fragment_with_warning.remove_warnings Not_All_Rows_Downloaded
102+
# `row_count` means another Database query is performed, so we only do it if we need to.
103+
all_rows_count = if has_more_rows then self.row_count else data_fragment_cleared.row_count
104+
Display_Helpers.display_table table=data_fragment_cleared add_row_index=False max_rows_to_show=show_rows all_rows_count=all_rows_count format_terminal=format_terminal
103105

104106
## PRIVATE
105107
ADVANCED
@@ -2941,37 +2943,6 @@ make_table connection table_name columns ctx on_problems =
29412943
problem_builder.attach_problems_before on_problems <|
29422944
DB_Table.Value table_name connection cols ctx
29432945

2944-
## PRIVATE
2945-
2946-
Renders an ASCII-art representation for a Table from a dataframe that
2947-
contains a fragment of the underlying data and count of all rows.
2948-
2949-
Arguments:
2950-
- df: The materialized dataframe that contains the data to be displayed, it
2951-
should have no indices set.
2952-
- indices_count: Indicates how many columns from the materialized dataframe
2953-
should be treated as indices in the display (index columns will be bold if
2954-
`format_terminal` is enabled).
2955-
- all_rows_count: The count of all rows in the underlying Table; if
2956-
`all_rows_count` is bigger than the amount of rows of `df`, an additional
2957-
line will be included that will say how many hidden rows there are.
2958-
- format_term: A boolean flag, specifying whether to use ANSI escape codes
2959-
for rich formatting in the terminal.
2960-
display_dataframe : Table -> Integer -> Integer -> Boolean -> Text
2961-
display_dataframe df indices_count all_rows_count format_terminal =
2962-
cols = Vector.from_polyglot_array df.java_table.getColumns
2963-
col_names = cols.map .getName . map normalize_string_for_display
2964-
col_vals = cols.map .getStorage
2965-
display_rows = df.row_count
2966-
rows = Vector.new display_rows row_num->
2967-
col_vals.map col->
2968-
if col.isNothing row_num then "Nothing" else get_item_string col row_num
2969-
table = print_table col_names rows indices_count format_terminal
2970-
if display_rows == all_rows_count then table else
2971-
missing_rows_count = all_rows_count - display_rows
2972-
missing = '\n\u2026 and ' + missing_rows_count.to_text + ' hidden rows.'
2973-
table + missing
2974-
29752946
## PRIVATE
29762947
By default, join on the first column, unless it's a cross join, in which
29772948
case there are no join conditions.

distribution/lib/Standard/Database/0.0.0-dev/src/Internal/Postgres/Postgres_Connection.enso

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ type Postgres_Connection
5252
Arguments:
5353
- connection: the underlying connection.
5454
- make_new: a function that returns a new connection.
55-
Value connection make_new
55+
private Value connection make_new
5656

5757
## ICON data_input
5858
Closes the connection releasing the underlying database resources
@@ -320,4 +320,3 @@ parse_postgres_encoding encoding_name =
320320
fallback.catch Any _->
321321
warning = Unsupported_Database_Encoding.Warning "The database is using an encoding ("+encoding_name.to_display_text+") that is currently not supported by Enso. Falling back to UTF-8. Column/table names may not be mapped correctly if they contain unsupported characters."
322322
Warning.attach warning Encoding.utf_8
323-

distribution/lib/Standard/Table/0.0.0-dev/src/Column.enso

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import project.Value_Type.Value_Type
3030
from project.Errors import Conversion_Failure, Floating_Point_Equality, Inexact_Type_Coercion, Invalid_Column_Names, Invalid_Value_Type, No_Index_Set_Error
3131
from project.Internal.Column_Format import all
3232
from project.Internal.Java_Exports import make_date_builder_adapter, make_string_builder
33-
from project.Table import print_table
3433

3534
polyglot java import org.enso.base.Time_Utils
3635
polyglot java import org.enso.table.data.column.operation.cast.CastProblemAggregator
@@ -155,19 +154,7 @@ type Column
155154
example_display = Examples.integer_column.display
156155
display : Integer -> Boolean -> Text
157156
display self show_rows=10 format_terminal=False =
158-
java_col = self.java_column
159-
col_name = normalize_string_for_display java_col.getName
160-
storage = java_col.getStorage
161-
num_rows = java_col.getSize
162-
display_rows = num_rows.min show_rows
163-
items = Vector.new display_rows num->
164-
row = if storage.isNothing num then "Nothing" else
165-
get_item_string storage num
166-
[num.to_text, row]
167-
table = print_table ["", col_name] items 1 format_terminal
168-
if num_rows - display_rows <= 0 then table else
169-
missing = '\n\u2026 and ' + (num_rows - display_rows).to_text + ' hidden rows.'
170-
table + missing
157+
self.to_table.display show_rows format_terminal
171158

172159
## PRIVATE
173160
ADVANCED

0 commit comments

Comments
 (0)