From 94488704617aa6c1826e09184c8d4733c0f15b8d Mon Sep 17 00:00:00 2001
From: Nick Woolmer <29717167+nwoolmer@users.noreply.github.com>
Date: Fri, 29 Nov 2024 15:58:19 +0000
Subject: [PATCH 1/6] clarify DEDUP sorting behaviour (#87)
@goodroot Perhaps this should be a section with a heading and not a
note? It ended up larger than expected.
---
documentation/concept/deduplication.md | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/documentation/concept/deduplication.md b/documentation/concept/deduplication.md
index d35fad91..e26fd49b 100644
--- a/documentation/concept/deduplication.md
+++ b/documentation/concept/deduplication.md
@@ -42,6 +42,29 @@ precisely, deduplicating the data based on the device ID can be expensive.
However, in cases where CPU metrics are sent at random and typically have unique
timestamps, the cost of deduplication is negligible.
+:::note
+
+The on-disk ordering of rows with duplicate timestamps differs when deduplication is enabled.
+
+- Without deduplication:
+ - the insertion order of each row will be preserved for rows with the same timestamp
+- With deduplication:
+ - the rows will be stored in order sorted by the `DEDUP UPSERT` keys, with the same timestamp
+
+For example:
+
+```questdb-sql
+DEDUP UPSERT keys(timestamp, symbol, price)
+
+-- will be stored on-disk in an order like:
+
+ORDER BY timestamp, symbol, price
+```
+
+This is the natural order of data returned in plain queries, without any grouping, filtering or ordering. The SQL standard does not guarantee the ordering of result sets without explicit `ORDER BY` clauses.
+
+:::
+
## Configuration
Create a WAL-enabled table with deduplication using
From 73d403a1be5f31fc97655f92ad8e7d14a18a7f13 Mon Sep 17 00:00:00 2001
From: goodroot <9484709+goodroot@users.noreply.github.com>
Date: Fri, 29 Nov 2024 08:20:27 -0800
Subject: [PATCH 2/6] Bump to questdb/sql-grammar to 1.1.0 (#88)
---
package.json | 2 +-
yarn.lock | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/package.json b/package.json
index 4576cfbf..9017a7b4 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
"@headlessui/react": "^2.2.0",
"@heroicons/react": "2.2.0",
"@mdx-js/react": "3.1.0",
- "@questdb/sql-grammar": "1.0.15",
+ "@questdb/sql-grammar": "1.1.0",
"@radix-ui/react-dialog": "1.1.2",
"@radix-ui/react-hover-card": "1.1.2",
"@radix-ui/react-slider": "1.2.1",
diff --git a/yarn.lock b/yarn.lock
index ea8aee97..bc37adcc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2477,10 +2477,10 @@
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.28.tgz#d45e01c4a56f143ee69c54dd6b12eade9e270a73"
integrity sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==
-"@questdb/sql-grammar@1.0.15":
- version "1.0.15"
- resolved "https://registry.yarnpkg.com/@questdb/sql-grammar/-/sql-grammar-1.0.15.tgz#a4b7ba0b8a100e29aeb33535b884b6186e17c8f2"
- integrity sha512-xduurxIv/cQgx3aHMIU1RYwyN1IBArZ2IUiVqAcoGwhVwmdmlDZkcYq5hkXnamvCFDu+Vzsg182xr+B/gpHbkw==
+"@questdb/sql-grammar@1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@questdb/sql-grammar/-/sql-grammar-1.1.0.tgz#75b46a86c21fe792cce8bd28950f5871085d3bdb"
+ integrity sha512-O4tD5PAMC/aV0qzNJ2QbDo6gWO71DqzEnp+EgxYei5PM01x3lAUm7+JhyMlQixoIHvo1i641F8GIjO0ErRTXfw==
"@radix-ui/number@1.1.0":
version "1.1.0"
From c22b533058a3cf7194b09a8eed0d5f6cf3e30604 Mon Sep 17 00:00:00 2001
From: glasstiger <94906625+glasstiger@users.noreply.github.com>
Date: Mon, 2 Dec 2024 16:59:06 +0000
Subject: [PATCH 3/6] Entra ID OIDC integration guide (#90)
Entra ID OIDC integration guide
---------
Co-authored-by: goodroot <9484709+goodroot@users.noreply.github.com>
---
.../guides/microsoft-entraid-oidc.md | 274 ++++++++++++++++++
documentation/sidebars.js | 8 +-
.../active-directory-entraid/10_overview.webp | Bin 0 -> 46182 bytes
.../1_app_registration.webp | Bin 0 -> 33668 bytes
.../2_spa_redirect_uri.webp | Bin 0 -> 44458 bytes
.../3_application_id.webp | Bin 0 -> 57874 bytes
.../active-directory-entraid/4_cors_pkce.webp | Bin 0 -> 49870 bytes
.../active-directory-entraid/5_ropc.webp | Bin 0 -> 46692 bytes
.../6_token_customization.webp | Bin 0 -> 46666 bytes
.../7_API_permissions.webp | Bin 0 -> 41788 bytes
.../8_add_openid_permissions.webp | Bin 0 -> 40008 bytes
.../9_permissions_final.webp | Bin 0 -> 43212 bytes
12 files changed, 281 insertions(+), 1 deletion(-)
create mode 100644 documentation/guides/microsoft-entraid-oidc.md
create mode 100644 static/images/guides/active-directory-entraid/10_overview.webp
create mode 100644 static/images/guides/active-directory-entraid/1_app_registration.webp
create mode 100644 static/images/guides/active-directory-entraid/2_spa_redirect_uri.webp
create mode 100644 static/images/guides/active-directory-entraid/3_application_id.webp
create mode 100644 static/images/guides/active-directory-entraid/4_cors_pkce.webp
create mode 100644 static/images/guides/active-directory-entraid/5_ropc.webp
create mode 100644 static/images/guides/active-directory-entraid/6_token_customization.webp
create mode 100644 static/images/guides/active-directory-entraid/7_API_permissions.webp
create mode 100644 static/images/guides/active-directory-entraid/8_add_openid_permissions.webp
create mode 100644 static/images/guides/active-directory-entraid/9_permissions_final.webp
diff --git a/documentation/guides/microsoft-entraid-oidc.md b/documentation/guides/microsoft-entraid-oidc.md
new file mode 100644
index 00000000..10107862
--- /dev/null
+++ b/documentation/guides/microsoft-entraid-oidc.md
@@ -0,0 +1,274 @@
+---
+title: Microsoft EntraID OIDC guide
+description: "QuestDB Enterprise guide to demonstrate Microsoft EntraID OpenID Connect."
+---
+
+import Screenshot from "@theme/Screenshot"
+
+This document sets up SSO authentication for the [QuestDB Web Console](/docs/web-console/) in
+[Microsoft EntraID](https://www.microsoft.com/en-gb/security/business/identity-access/microsoft-entra-id), formerly known as Azure AD.
+
+For a general introduction to OpenID Connect and QuestDB, see the
+[OIDC Operations page](/docs/operations/openid-connect-oidc-integration/).
+
+:::tip
+
+To enlarge the images, click or tap them.
+
+:::
+## Set up the client application in Entra ID
+
+First thing first, let's pick a name for the client!
+
+Then head to _Microsoft Entra Admin Center_, and register the application
+under _Identity - App registrations - New registration_.
+
+
+
+The QuestDB [Web Console](/docs/web-console/) is a SPA (Single Page App).
+
+As a result, it cannot store safely a client secret.
+
+Instead, it can use PKCE (Proof Key for Code Exchange) to secure the flow.
+
+When registering the application, select the SPA platform.
+
+We also have to specify the URL of the [Web Console](/docs/web-console/) as Redirect URI.
+
+
+
+After clicking _Register_, we have created a client application with the
+name _QuestDB_.
+
+Each application is assigned a unique id (known as Client ID in the
+OAuth2 - OIDC standard). The client will identify itself with this id
+when sending requests to Entra ID.
+
+
+
+We find the platform configurations under _Authentication_. This is the place where
+the previously set redirect URI can be viewed and modified. We can also specify
+additional redirect URIs, if necessary.
+
+The redirect URIs of the application are automatically eligible for the
+_Authorization Code Flow with PKCE_, which is a special version of the OAuth2 standard's
+Authorization Code Flow. It is specifically designed for applications where a client
+secret (e.g. a password) could not be kept safely. As single page applications run in
+the browser, they fall into this category.
+
+The redirect URIs are also added to the _CORS_ (Cross-Origin Resource Sharing) policy
+of EntraID. CORS is a mechanism to allow a web page, such as the Web Console, to access
+resources from a different domain than the one that served the page. In this context
+this means that we let the Web Console to access Entra ID, while its origin is the
+HTTP endpoint of QuestDB.
+
+
+
+If we scroll down to the bottom of this page, we can also find a section where we
+can enable the _Resource Owner Password Credential Flow_.
+
+This OAuth2 flow is legacy, and should be enabled only if there is a requirement
+of connecting to QuestDB using SSO (Single Sign-On) via clients not supporting
+redirect based web flows.
+This could mean a Postgres client without OAuth2 integration, such as _psql_, or
+a standalone in-house client application, or could be just a jupyter notebook.
+
+The main issue with this flow is that the client application has to be trusted
+with the user's login details. The user's credentials are passed to the
+application, in this case to QuestDB, and the client application uses these
+credentials to authenticate the user by forwarding them to the identity provider,
+in this case to Entra ID.
+
+It is guaranteed that QuestDB does not store the user's credentials in any way.
+They are not persisted into the database, not even in encrypted form.
+The login details are treated as passthrough information. Only exception is
+that server logs can contain the username, logged for audit purposes.
+
+
+
+Our next stop is the _Token configuration_, where the OAuth2/OIDC access and ID
+tokens can be customized.
+
+Note that users can be authenticated without customized tokens, but authorization
+would prove to be challenging. The user's security groups are not included
+in the tokens by default.
+
+QuestDB can be configured to request the user's groups from the UserInfo
+endpoint of the OAuth2 server, but Entra ID cannot be configured to provide
+this information via the UserInfo endpoint.
+Therefore, we choose to customize the tokens, QuestDB will decode and
+validate the ID token, and take the group information from there.
+
+QuestDB authorization relies on receiving the group memberships of the user.
+Entra ID groups should be mapped to QuestDB groups, and permissions can be
+granted to the QuestDB groups. Detailed information about group mappings can
+be found in the [OIDC integration](/docs/operations/openid-connect-oidc-integration/#user-permissions)
+documentation.
+
+
+
+The customized tokens contain user information which cannot be accessed
+without permission. User information is provided by Microsoft Graph, so
+the client application needs specific permissions to access
+Microsoft Graph APIs.
+
+These permissions can be configured under _API permissions_. It is important
+to note that we will be setting _Delegated_ permissions here, meaning we
+are not granting actual permissions to access user data. Instead, each user
+logging into QuestDB will have to consent to accessing their user profile.
+
+
+
+By default, the _User.Read_ permission is added to the list, but what we
+really need is:
+ - openid: to be able to issue ID tokens
+ - profile: to access user information
+ - offline_access: to be able to issue refresh tokens
+
+By clicking on _Microsoft Graph_ we can select and add these permissions.
+
+
+
+The _User.Read_ permission is not needed. It can be removed by clicking
+on the `...` at the end of the row, and selecting _Remove permission_ from
+the popup menu.
+
+
+
+With this we have finished setting up the QuestDB client application
+in Entra ID, and now we can wire QuestDB and Entra ID together by
+adding OIDC configuration to QuestDB.
+
+## QuestDB configuration
+
+The below should be set in QuestDB's `server.conf`:
+
+```shell
+# enable OIDC
+acl.oidc.enabled=true
+
+# the claim contains the username or user id
+acl.oidc.sub.claim=name
+
+# the claim contains the user's group memberships
+acl.oidc.groups.claim=groups
+
+# groups are encoded in the token
+acl.oidc.groups.encoded.in.token=true
+
+# OIDC configuration endpoint of Entra ID
+acl.oidc.configuration.url=https://login.microsoftonline.com/12345678-1234-1234-1234-123456789abc/v2.0/.well-known/openid-configuration
+
+# application ID taken from Entra ID
+acl.oidc.client.id=8de84b90-1ea5-4e41-9e84-dba860aa01a6
+
+# redirect URI, QuestDB's HTTP endpoint
+acl.oidc.redirect.uri=http://localhost:9000
+
+# OAuth scopes the user has to consent to
+acl.oidc.scope=openid profile offline_access
+
+# enable ROPC flow
+# optional, required only if ROPC is enabled in Entra ID
+acl.oidc.ropc.flow.enabled=true
+```
+
+The application ID and the OIDC configuration endpoint's URL can be found
+in the Overview of the application in Entra ID.
+
+The application ID is displayed right under the application's name, the
+OIDC configuration endpoint is displayed on the panel which opens up when
+the _Endpoints_ button is clicked.
+
+
+
+## Map groups and grant permissions
+
+Now we can start QuestDB, and login with the built-in admin to create
+group mappings.
+
+As mentioned earlier, authorization works by mapping Entra ID groups
+to QuestDB groups. When the user logs in, QuestDB decodes Entra ID
+group memberships from the token, then finds the QuestDB groups
+mapped to them, and the user gets the permissions based on the
+mapped groups.
+
+```questdb-sql title="Create a group which is mapped to an Entra ID group"
+CREATE GROUP extUsers WITH EXTERNAL ALIAS '87654321-1234-1234-1234-123456789abc';
+```
+The above command maps the Entra ID group identified by object
+id `87654321-1234-1234-1234-123456789abc` to a QuestDB group called `extUsers`.
+
+We should grant the necessary QuestDB endpoint permissions first
+to make sure users can access the Web Console, Postgres and ILP
+interfaces as required. [Read more about endpoint permissions](/docs/operations/rbac/#endpoint-permissions).
+
+```questdb-sql title="Grant endpoint permissions"
+GRANT HTTP, PGWIRE TO groupName;
+```
+
+Now we can grant the rest of the permissions as required. We can
+grant access to tables, for example.
+
+```questdb-sql title="Grant database permissions"
+GRANT SELECT ON table1, table2 to groupName;
+```
+
+## Confirm group mappings and login
+
+To test, head to the Web Console and login.
+
+If all has been wired up well, then login will succeed, and the user
+will have the access granted to them.
+
+
diff --git a/documentation/sidebars.js b/documentation/sidebars.js
index 6d9f134e..4c1e8fb7 100644
--- a/documentation/sidebars.js
+++ b/documentation/sidebars.js
@@ -376,9 +376,15 @@ module.exports = {
label: "Guides & Tutorials",
type: "category",
items: [
+ {
+ id: "guides/microsoft-entraid-oidc",
+ label: "Active Directory - Microsoft Entra ID",
+ type: "doc",
+ customProps: { tag: "Enterprise" },
+ },
{
id: "guides/active-directory-pingfederate",
- label: "Active Directory",
+ label: "Active Directory - PingFederate",
type: "doc",
customProps: { tag: "Enterprise" },
},
diff --git a/static/images/guides/active-directory-entraid/10_overview.webp b/static/images/guides/active-directory-entraid/10_overview.webp
new file mode 100644
index 0000000000000000000000000000000000000000..2077228be71b66f80291cc74afba1b70887542d1
GIT binary patch
literal 46182
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