Compare commits

...
Author SHA1 Message Date
Claude 9c8c761529 fix(auth): environment-scoped session cookies and resilient /auth/me refresh
Apps behind an nginx auth_request (Orchest / Intelligence / Process) rely on
the ddf-* cookies and on GET /auth/me. Several problems made them lose the
session or show 500 pages:

- Cookies were written with a hard-coded Domain=.dadosfera.ai in every
  environment, so a staging login overwrote the production session (and
  vice versa) in the same browser. Names now take AUTH_COOKIE_SUFFIX and the
  domain comes from COOKIE_DOMAIN (staging: -stg / .stg.dadosfera.ai).
- maxAge was the token's absolute epoch (~56 years). It is now a real
  duration, SESSION_COOKIE_MAX_AGE_DAYS (default 30).
- /auth/me answered 500 when a refresh failed for anything but
  NotAuthorizedException, when the refreshed token failed validation, or
  when DUC hung (no deadline). It now answers 401 and expires the dead
  cookies for definitive failures, 503 for DUC transport errors/timeouts
  (AUTH_RPC_TIMEOUT_MS, default 10s), keeps 403 answers instead of masking
  them with a refresh, refreshes when only the refresh cookie is left, and
  stops logging request headers (they contain the cookies).
- /auth/refresh-access-token rewrites all three cookies from the session the
  frontend just proved valid, healing stale cookies from another login or
  environment. /auth/sign-out expires all three and always responds.

Helm values wire the new variables for prd and stg.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUEXmydTBX9G2CGJ97YQKL
2026-09-03 13:13:54 +00:00
marcos.rodrigues 740540d4dc Merge pull request #517 from dadosfera/bugfix/sync-proto
FIX: sync proto
2026-08-28 13:33:27 -03:00
marcos.rodrigues 94c9ab7939 FIX: add missing helm value 2026-08-28 12:25:44 -03:00
marcos.rodrigues a211b8593e FIX: sync proto 2026-08-28 12:23:25 -03:00
marcos.rodrigues 25661613d5 Merge pull request #516 from dadosfera/bugfix/add-jest-file-in-exclude-build
FIX: add-jest-file-in-exclude-build
2026-08-27 17:29:08 -03:00
marcos.rodrigues 3d8ea3576d FIX: add-jest-file-in-exclude-build 2026-08-27 17:10:37 -03:00
marcos.rodrigues e3a2eba5c3 Merge pull request #515 from dadosfera/release/2026-08-27-beta
Release/2026 08 27 beta
2026-08-27 16:09:16 -03:00
marcos.rodrigues 81c446354b FIX: mock process.env and dadosfera logger 2026-08-27 14:33:30 -03:00
RafaelandWOZCODE bf0314f5b1 FIX: trigger release for /auth/me permission seqids (PR #513)
PR #513 merged to beta but no semantic-release ran: its commits used
conventional-commits prefixes (feat(auth):, fix(auth):) which the
.releaserc.json eslint preset does not recognise, so commit-analyzer
found no release-worthy change. This empty FIX: commit matches the
eslint preset's releaseRules (tag FIX -> patch) to cut a beta release
that includes the /auth/me permission-seqids change, so stg can deploy it.

Co-Authored-By: WOZCODE <contact@withwoz.com>
2026-08-24 19:18:59 -03:00
Rafael Santana c97180ac95 Merge pull request #513 from dadosfera/feat/auth-me-orchest-identity
feat(auth): return the user's permission seqids from /auth/me
2026-08-24 17:00:15 -03:00
RafaelandWOZCODE cac36f2c60 refactor(auth): /auth/me returns raw permission seqids
Return payload.permissions verbatim (numeric seqids) instead of
translating them to claim strings. Consumers own the seqid->meaning
mapping. Drops permission-claims.ts entirely; UserDTO.permissions is
now number[].

Co-Authored-By: WOZCODE <contact@withwoz.com>
2026-08-24 16:56:23 -03:00
RafaelandWOZCODE cd21fd0b7b refactor(auth): /auth/me returns permissions only (drop roles/modules)
Keep Maestro a pure identity provider: /auth/me exposes the user's
permission claim strings and nothing consumer-specific. Consumers derive
whatever meaning they need (roles, module access, groups) from the claim
vocabulary — claims are already namespaced group:action.

- UserDTO: drop roles[]/modules[], keep permissions[].
- Helper shrinks to a generic seqid->claim translation
  (orchest-identity.ts -> permission-claims.ts, translateSeqidsToClaims).
- api-key branch: permissions: [] only.

The roles/modules derivation moves entirely to the consumer (Orchest's
auth-server adapter).

Co-Authored-By: WOZCODE <contact@withwoz.com>
2026-08-24 16:30:33 -03:00
RafaelandWOZCODE a5a685ee3f fix(auth): derive Orchest identity from numeric seqids (JWT carries seqids not claim strings)
The JWT `permissions` claim is an array of numeric seqids at runtime
(see authentication.guard.ts / authentication.decorator.ts), not claim
strings. deriveOrchestIdentity previously matched claim strings against
this numeric array, so roles[]/modules[] were always empty for every
real user.

- deriveOrchestIdentity now takes number[] | undefined and matches
  seqids sourced from PERMISSIONS_GROUPS (permissions.enum.ts) instead
  of hand-copied literals.
- permissions is translated back to claim strings via a full
  seqid->claim catalog built once from PERMISSIONS_GROUPS; unknown
  seqids are dropped (auth-server ignores permissions[] in v1).
- auth.controller.ts's api-key branch literal is now annotated
  `: UserDTO` so tsc enforces the three fields there.
- Both spec files re-fixtured with numeric seqid inputs, including a
  mixed admin+module case and an exact claim-string translation
  assertion.

Co-Authored-By: WOZCODE <contact@withwoz.com>
2026-08-24 16:03:20 -03:00
Rafael a16fefe691 feat(auth): return permissions/roles/modules from /auth/me (all branches) 2026-08-24 15:45:09 -03:00
Rafael 9c57485031 feat(auth): pure helper deriving Orchest identity from permissions 2026-08-24 15:39:40 -03:00
marcos.rodrigues 7051b21d86 Merge pull request #509 from dadosfera/release/2026-08-12
FIX: require collect module in endpoints
2026-08-12 15:06:04 -03:00
marcos.rodrigues 0c4888ccdb FIX: require collect module in endpoints 2026-08-12 15:02:37 -03:00
marcos.rodrigues e1b0e88bd8 Merge pull request #507 from dadosfera/bugfix/catalog-module-main
Bugfix/catalog module main
2026-08-12 09:59:08 -03:00
marcos.rodrigues cef1184908 Merge branch 'beta' into bugfix/catalog-module-main 2026-08-12 09:58:58 -03:00
marcos.rodrigues 31dda867d1 FIX: require collect module in endpoints 2026-08-12 09:57:23 -03:00
marcos.rodrigues 8a92da470c Merge pull request #506 from dadosfera/bugfix/catalog-module-main
FIX: skip nimbus update when customer haven't catalog module
2026-08-07 16:27:19 -03:00
marcos.rodrigues b89909ad66 FIX: skip nimbus update when customer haven't catalog module 2026-08-07 14:48:46 -03:00
marcos.rodrigues 2bb280e8de Merge pull request #505 from dadosfera/bugfix/catalog-module
Bugfix/catalog module
2026-08-07 12:26:21 -03:00
25 changed files with 839 additions and 121 deletions
@@ -101,6 +101,17 @@ spec:
value: {{ .Values.maestro.dedicated_proxy }}
- name: COOKIE_SECRET
value: {{ .Values.maestro.cookie_secret }}
# Browser session cookies (ddf-auth*, ddf-refresh-auth*, ddf-user-id*).
# Scoped per environment so a staging login never overwrites a
# production session in the same browser (both live under dadosfera.ai).
- name: COOKIE_DOMAIN
value: {{ .Values.maestro.cookie_domain | default ".dadosfera.ai" | quote }}
- name: AUTH_COOKIE_SUFFIX
value: {{ .Values.maestro.auth_cookie_suffix | default "" | quote }}
- name: SESSION_COOKIE_MAX_AGE_DAYS
value: {{ .Values.maestro.session_cookie_max_age_days | default "30" | quote }}
- name: AUTH_RPC_TIMEOUT_MS
value: {{ .Values.maestro.auth_rpc_timeout_ms | default "10000" | quote }}
- name: REDIS_DATABASE
value: "{{ .Values.maestro.redis_database }}"
- name: REDIS_HOST
+6
View File
@@ -7,6 +7,12 @@ maestro:
open_customer_id: b3e3dfe5-b992-4586-a73c-c0b0c00f615d
open_group_id: e3f98a2f-7748-4981-8505-7695c8ca8218
cookie_secret: "ff7bc13823edb2ae50d248e5780bddc9d4b31c36"
# Session cookies: staging gets its own names AND domain so that
# app.stg.dadosfera.ai and app.dadosfera.ai sessions never collide.
cookie_domain: ".stg.dadosfera.ai"
auth_cookie_suffix: "-stg"
session_cookie_max_age_days: "30"
auth_rpc_timeout_ms: "10000"
redis_database: "1"
platform_api_url: https://xs2hkhq07k.execute-api.us-east-1.amazonaws.com
connections_api_url: https://iy40eans64.execute-api.us-east-1.amazonaws.com
+7
View File
@@ -55,7 +55,14 @@ maestro:
redis_database: "0"
redis_tls: "true"
cookie_secret: "13cc5e136d3074bcc05bec8697092ec1f5f376bf"
# Session cookies: production keeps the historical names on the apex domain.
cookie_domain: ".dadosfera.ai"
auth_cookie_suffix: ""
session_cookie_max_age_days: "30"
auth_rpc_timeout_ms: "10000"
firebase_base_url: https://feature-flag-25bf6-default-rtdb.firebaseio.com/prd
connections_api_url: https://wv6fw7ayj4.execute-api.us-east-1.amazonaws.com
autoscaling:
enabled: false
minReplicas: 1
+234
View File
@@ -900,6 +900,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -949,6 +952,9 @@
"connections"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -997,6 +1003,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -1033,6 +1042,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -1066,6 +1078,9 @@
"connections"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -2403,6 +2418,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -2486,6 +2504,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -2525,6 +2546,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -2564,6 +2588,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -2601,6 +2628,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -2640,6 +2670,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -2681,6 +2714,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -2722,6 +2758,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -3071,6 +3110,9 @@
"PipelinesV2"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -3120,6 +3162,9 @@
"PipelinesV2"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -3190,6 +3235,9 @@
"PipelinesV2"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -3230,6 +3278,9 @@
"PipelinesV2"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -3270,6 +3321,9 @@
"PipelinesV2"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -3310,6 +3364,9 @@
"PipelinesV2"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -3350,6 +3407,9 @@
"PipelinesV2"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -3397,6 +3457,9 @@
"PipelinesV2"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -3442,6 +3505,9 @@
"PipelinesV2"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -3490,6 +3556,9 @@
"PipelinesV2"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -3528,6 +3597,9 @@
"PipelinesV2"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -3586,6 +3658,9 @@
"PipelinesV2"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -3634,6 +3709,9 @@
"PipelinesV2"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -3674,6 +3752,9 @@
"PipelinesV2"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -3716,6 +3797,9 @@
"PipelinesV2"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -3765,6 +3849,9 @@
"PipelinesV2"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -3814,6 +3901,9 @@
"PipelinesV2"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -3861,6 +3951,9 @@
"PipelinesV2"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4031,6 +4124,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4056,6 +4152,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4092,6 +4191,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4126,6 +4228,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4160,6 +4265,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4187,6 +4295,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4214,6 +4325,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4241,6 +4355,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4277,6 +4394,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4313,6 +4433,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4340,6 +4463,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4367,6 +4493,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4404,6 +4533,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4440,6 +4572,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4484,6 +4619,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4520,6 +4658,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4564,6 +4705,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4656,6 +4800,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4690,6 +4837,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4726,6 +4876,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4762,6 +4915,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4799,6 +4955,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4826,6 +4985,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -4853,6 +5015,9 @@
"Platform API"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -5264,6 +5429,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -5307,6 +5475,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -5416,6 +5587,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -5448,6 +5622,9 @@
"Catalog"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -5490,6 +5667,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -5625,6 +5805,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -5683,6 +5866,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -5731,6 +5917,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -5824,6 +6013,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -5874,6 +6066,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -5932,6 +6127,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -5988,6 +6186,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -6048,6 +6249,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -6088,6 +6292,9 @@
"Catalog"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -6128,6 +6335,9 @@
"Catalog"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -6188,6 +6398,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -6246,6 +6459,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -6622,6 +6838,9 @@
{
"access-token": []
},
{
"access-token": []
},
{
"access-token": []
}
@@ -7075,6 +7294,9 @@
"Connection Test"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -7111,6 +7333,9 @@
"Connection Test"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -7147,6 +7372,9 @@
"Connection Test"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -7183,6 +7411,9 @@
"Connection Test"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
@@ -7219,6 +7450,9 @@
"Connection Test"
],
"security": [
{
"access-token": []
},
{
"access-token": []
}
+1
View File
@@ -11,6 +11,7 @@ const config: Config.InitialOptions = {
'<rootDir>/node_modules/',
'.*\\.module\\.[jt]s$',
],
setupFiles: ['<rootDir>/jest.setup.ts'],
// moduleDirectories: ['node_modules'], // default is already 'node_modules'
// rootDir: '.', //No need
// testEnvironment: 'node', //Defaults to 'node'
+3
View File
@@ -0,0 +1,3 @@
process.env.DUC_URL="duc:50051"
process.env.INFACTORY_URL="in-factory:50052"
process.env.PIFACTORY_URL="pi-factory:50053"
+5 -4
View File
@@ -16,7 +16,7 @@
"@aws-sdk/lib-dynamodb": "^3.414.0",
"@aws-sdk/signature-v4": "^3.370.0",
"@dadosfera/dadosfera-logs": "^1.0.0-beta.4",
"@dadosfera/protospack-v2": "^3.40.0-beta.14",
"@dadosfera/protospack-v2": "3.40.0",
"@grpc/grpc-js": "^1.9.3",
"@grpc/proto-loader": "^0.7.9",
"@nestjs/cli": "^9.5.0",
@@ -1735,9 +1735,10 @@
}
},
"node_modules/@dadosfera/protospack-v2": {
"version": "3.40.0-beta.14",
"resolved": "https://dadosfera-611330257153.d.codeartifact.us-east-1.amazonaws.com/npm/dadosfera-npm/@dadosfera/protospack-v2/-/protospack-v2-3.40.0-beta.14.tgz",
"integrity": "sha512-pv3pxq0x1XcBgf3ajD6QOFRLOduh8iEozKFA3AKlIW4gid+gT4iL0GcU2M+O7h0QFeO4JIzRZe/nEMN82nqk7A==",
"version": "3.40.0",
"resolved": "https://dadosfera-611330257153.d.codeartifact.us-east-1.amazonaws.com/npm/dadosfera-npm/@dadosfera/protospack-v2/-/protospack-v2-3.40.0.tgz",
"integrity": "sha512-tzYAGosFjGUgi3pAvXqv+Eqqjca9tH1kU9kJdRI5d1rLXNimo0fIuPWxWng0C4OBH/2sIgC1WLHseq+DZ6qgdw==",
"license": "ISC",
"dependencies": {
"@grpc/grpc-js": "^1.9.3",
"rxjs": "^7.5.5"
+1 -1
View File
@@ -34,7 +34,7 @@
"@aws-sdk/lib-dynamodb": "^3.414.0",
"@aws-sdk/signature-v4": "^3.370.0",
"@dadosfera/dadosfera-logs": "^1.0.0-beta.4",
"@dadosfera/protospack-v2": "^3.40.0-beta.14",
"@dadosfera/protospack-v2": "3.40.0",
"@grpc/grpc-js": "^1.9.3",
"@grpc/proto-loader": "^0.7.9",
"@nestjs/cli": "^9.5.0",
+2
View File
@@ -722,6 +722,8 @@ export const DADOSFERA_MODULES_KEYS = {
PII: 'pii',
EMBED: 'embedded-analytics',
EMBED_ASSIGNED: 'embed-assigned',
CATALOG: 'catalog',
COLLECT: 'collect',
}
export const DADOSFERA_MODULES: Array<DadosferaModule> = [
+42 -1
View File
@@ -35,4 +35,45 @@ So in order to have an SSO login, besides configuring the Strategy, we must have
// ... Rest of the logic
return { url: url.href };
}
```
```
## Browser session cookies
Besides the tokens returned in the sign-in body (kept by the frontend in
`localStorage` and sent as `Authorization`), Maestro also persists the session
as httpOnly cookies so that apps which cannot hold a token themselves —
Orchest / Intelligence / Process behind an nginx `auth_request` pointing at
`GET /auth/me` — can authenticate the browser.
| Cookie | Content |
| --- | --- |
| `ddf-auth<suffix>` | DUC access token |
| `ddf-refresh-auth<suffix>` | IdP refresh token |
| `ddf-user-id<suffix>` | DUC user id |
All three are `HttpOnly; Secure; SameSite=None; Path=/` and live for
`SESSION_COOKIE_MAX_AGE_DAYS` (default 30 — keep it aligned with the IdP
refresh-token validity). See `session-cookies.ts`.
They are **environment-scoped** so that a login on `app.stg.dadosfera.ai`
never overwrites a production session in the same browser:
| Env | `AUTH_COOKIE_SUFFIX` | `COOKIE_DOMAIN` |
| --- | --- | --- |
| prd | `""` | `.dadosfera.ai` |
| stg | `-stg` | `.stg.dadosfera.ai` |
`GET /auth/me` behaviour:
- valid access token → `200` + user (permissions are the raw seqids from the JWT);
- expired/invalid access token but refresh token present → refresh, rewrite the
cookies (`Set-Cookie`, access token first — nginx `auth_request` only relays
the first one) and `200`;
- refresh impossible (expired/revoked/foreign token, unknown user) → `401` and the
cookies are expired so the browser stops presenting a dead session;
- DUC unreachable / slower than `AUTH_RPC_TIMEOUT_MS` (default 10s) → `503`,
cookies untouched (transient, retry);
- resource belongs to another customer → `403`; new terms of use pending → `403`.
`POST /auth/refresh-access-token` re-writes all three cookies from the session
the frontend is actually using, and `POST /auth/sign-out` expires them.
+123 -35
View File
@@ -14,6 +14,8 @@ import {
Param,
Res,
UnauthorizedException,
HttpException,
ServiceUnavailableException,
} from '@nestjs/common';
import {
ApiHeaders,
@@ -37,6 +39,7 @@ import {
RequireAllPermissions,
} from 'src/decorators/authentication.decorator';
import { AuthClientService } from './auth.service';
import { UserDTO } from './dtos/login';
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import { GrpcToHttpExceptionFilter } from '../../error/grpc-to-http-exception.filter';
import { RequestUser, User } from 'src/decorators/user.decorator';
@@ -50,7 +53,10 @@ import {
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
import { AuthGuard } from '@nestjs/passport';
import { Request, Response } from 'express';
import { status as GrpcStatus } from '@grpc/grpc-js';
import ErrorCodes, { OauthErrors } from 'src/utils/errorCodes';
import ErrorBuilder from 'src/utils/ErrorBuilder';
import { clearSessionCookies, readAuthSession } from './session-cookies';
import jwt, { JwtPayload } from 'jsonwebtoken';
import { LanguageEnum } from 'src/utils/languages.enum';
import { Language } from 'src/decorators/language.decorator';
@@ -128,14 +134,16 @@ export class AuthController {
) {
try {
this.logger.info('/auth - SignOut');
this.authClient.cleanUpAuthSession(res);
return res.send();
this.authClient.cleanUpAuthSession(res);
} catch (error) {
this.logger.error('/auth - SignIn - ERROR', error);
this.logger.error('/auth - SignOut - ERROR', error);
}
// Always answer: a sign-out that hangs leaves the frontend logout
// waiting on a request that never completes.
return res.send();
}
@Post('refresh-access-token')
@@ -148,7 +156,7 @@ export class AuthController {
@Res() res: Response,
) {
this.logger.info('/auth - RefreshAccessToken');
const frontHost = origin.replace(/^https?:\/\//, '');
const frontHost = (origin || '').replace(/^https?:\/\//, '');
const { refreshToken, userId } = body;
const metadata = PackTheMetadata({
@@ -158,10 +166,14 @@ export class AuthController {
const data = await this.authClient.refreshAccessToken({ refreshToken, userId }, metadata);
// Re-sync ALL session cookies with the session the frontend is actually
// using. The IdP does not rotate refresh tokens (DUC returns an empty
// refreshToken), so fall back to the one that just proved valid — this
// heals cookies left behind by an older login or by another environment.
this.authClient.writeAuthSession(res, {
accessToken: data.accessToken,
refreshToken: data.refreshToken,
userId
refreshToken: data.refreshToken || refreshToken,
userId,
});
return res.send(data);
@@ -477,16 +489,28 @@ export class AuthController {
@Get('me')
async getMe(@Req() req: Request, @Res() res: Response) {
this.logger.info('GET /auth/me ')
this.logger.info(JSON.stringify(req.headers));
const resourceHost = (req.headers['x-original-url'] as string) || '';
// Never dump req.headers here: it contains the session cookies.
this.logger.info('GET /auth/me', { resourceHost });
// Check for API key header first
const apiKey = req.get('X-Api-key');
if (apiKey) {
this.logger.info('Authenticating via X-Api-key header');
const { api_key } = await this.apiKeyService.get(apiKey);
let api_key: Awaited<ReturnType<ApiKeyService['get']>>['api_key'];
try {
({ api_key } = await this.apiKeyService.get(apiKey));
} catch (error) {
if (this.isTransportError(error)) {
throw this.identityProviderUnavailable(error);
}
this.logger.error('/auth/me - invalid API key', {
error: error?.message,
});
throw new UnauthorizedException('Invalid API key');
}
const userDto = {
const userDto: UserDTO = {
id: api_key.user_id,
name: api_key.username,
email: api_key.username,
@@ -494,41 +518,105 @@ export class AuthController {
id: api_key.customer_id,
name: api_key.customer_name,
tier: api_key.customer_tier,
}
},
permissions: [],
};
return res.status(200).json(userDto);
}
// Get token and headers
const accessToken = req.cookies['ddf-auth'];
const refreshToken = req.cookies['ddf-refresh-auth'];
const userId = req.cookies['ddf-user-id'];
const resourceHost = req.headers["x-original-url"] as string || "" ;
const { accessToken, refreshToken, userId } = readAuthSession(req);
const canRefresh = Boolean(refreshToken) && Boolean(userId);
this.logger.info('Has User Session: ' + Boolean(accessToken || canRefresh));
const hasUserSession = Boolean(accessToken) && Boolean(userId);
this.logger.info('Has User Session: ' + hasUserSession);
if (!accessToken && !canRefresh) {
throw new UnauthorizedException();
}
if (!hasUserSession) {
throw new UnauthorizedException()
if (accessToken) {
try {
const userDto = await this.authClient.validateUserSession(
accessToken,
resourceHost,
);
return res.status(200).json(userDto);
} catch (error) {
// Definitive answers (e.g. 403: resource belongs to another
// customer) must not be masked by a refresh attempt.
if (error instanceof HttpException) throw error;
if (this.isTransportError(error)) {
throw this.identityProviderUnavailable(error);
}
if (!canRefresh) {
this.logger.error(
'/auth/me - access token rejected and no refresh token',
{ error: error?.message },
);
clearSessionCookies(res);
throw new ErrorBuilder(ErrorCodes.AUTH.INVALID_REFRESH_TOKEN);
}
this.logger.info('/auth/me - access token rejected, refreshing', {
error: error?.message,
});
}
}
try {
const userDto = await this.authClient.validateUserSession(accessToken, resourceHost);
return res.status(200).json(userDto);
} catch (error) {
if (!refreshToken) {
this.logger.error('Invalid refresh token or customer name');
throw new UnauthorizedException("Invalid refresh token or customer name");
};
const {
authSession,
user
} = await this.authClient.refreshUserSession(refreshToken, userId, resourceHost);
this.authClient.writeAuthSession(res, authSession);
const { authSession, user } = await this.authClient.refreshUserSession(
refreshToken,
userId,
resourceHost,
);
// Refresh tokens are not rotated by the IdP (DUC returns ''), so keep
// the one that just worked; this also slides the cookie lifetime.
this.authClient.writeAuthSession(res, {
...authSession,
refreshToken: authSession.refreshToken || refreshToken,
});
return res.status(200).json(user);
} catch (error) {
if (error instanceof HttpException) throw error;
if (this.isTransportError(error)) {
throw this.identityProviderUnavailable(error);
}
const code =
typeof error?.details === 'string' ? error.details : undefined;
if (code === ErrorCodes.TERMS_OF_USE.REQUIRED) {
// The session is fine; the user has to accept the new terms first.
throw new ErrorBuilder(code);
}
// Expired, revoked, or minted by another environment / user pool.
// Answer 401 (never 500) and drop the dead cookies so the browser
// does not keep presenting them on every request.
this.logger.error('/auth/me - session refresh failed', {
code,
error: error?.message,
});
clearSessionCookies(res);
throw new ErrorBuilder(ErrorCodes.AUTH.INVALID_REFRESH_TOKEN);
}
}
/** DUC unreachable or too slow — a transient failure, not an auth answer. */
private isTransportError(error: any): boolean {
return (
error?.name === 'TimeoutError' ||
error?.code === GrpcStatus.UNAVAILABLE ||
error?.code === GrpcStatus.DEADLINE_EXCEEDED
);
}
private identityProviderUnavailable(error: any): HttpException {
this.logger.error('/auth/me - identity provider unavailable', {
code: error?.code,
error: error?.message,
});
// 503 (not 500): callers such as nginx auth_request / Orchest's
// auth-server treat it as "retry later" instead of a broken session.
return new ServiceUnavailableException(
'Identity provider temporarily unavailable',
);
}
}
+43 -69
View File
@@ -8,7 +8,7 @@ import {
} from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import { lastValueFrom } from 'rxjs';
import { lastValueFrom, timeout } from 'rxjs';
import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc';
import {
@@ -35,12 +35,24 @@ import { BulkEditResponse, UserDTO } from './dtos/login';
import jwt, { JwtPayload } from 'jsonwebtoken';
import { PackTheMetadata } from 'src/utils/PackTheMetadata';
import { Request, Response } from 'express';
import {
AuthSession,
clearSessionCookies,
writeSessionCookies,
} from './session-cookies';
type AuthSession = {
accessToken?: string;
refreshToken?: string;
userId?: string;
};
export type { AuthSession };
/**
* Upper bound for the DUC round-trips on the session-validation path
* (`GET /auth/me`). Without it a hung DUC keeps every `auth_request`
* subrequest open until the ingress gives up (minutes), which surfaces to
* users as 500/504 pages instead of a fast, retryable 503.
*/
function authRpcTimeoutMs(): number {
const ms = Number(process.env.AUTH_RPC_TIMEOUT_MS);
return Number.isFinite(ms) && ms > 0 ? ms : 10_000;
}
@Injectable()
export class AuthClientService implements OnModuleInit {
@@ -70,7 +82,9 @@ export class AuthClientService implements OnModuleInit {
async getPublicKeys() {
this.logger.info('GetPublicKeys');
return lastValueFrom(this.authService.AuthGetPublicKeys({}));
return lastValueFrom(
this.authService.AuthGetPublicKeys({}).pipe(timeout(authRpcTimeoutMs())),
);
}
async snowflakeSignIn(input: AuthSnowflakeSignInRequest) {
@@ -132,10 +146,9 @@ export class AuthClientService implements OnModuleInit {
this.logger.info('RefreshAccessToken');
return lastValueFrom(
this.authService.AuthRefreshAccessToken(
{ refreshToken, userId },
metadata,
),
this.authService
.AuthRefreshAccessToken({ refreshToken, userId }, metadata)
.pipe(timeout(authRpcTimeoutMs())),
);
}
@@ -346,67 +359,18 @@ export class AuthClientService implements OnModuleInit {
};
}
/**
* Persists the session in the browser as httpOnly cookies. Names, domain
* and lifetime are environment-scoped — see `session-cookies.ts`.
*/
public writeAuthSession(res: Response, data: AuthSession) {
let exp = 1000 * 60 * 5; // 5 minutes
if (data.accessToken) {
const { exp: expiration } = jwt.decode(data.accessToken) as JwtPayload;
exp = (expiration - 30) * 1000; // exp em segundos, maxAge em ms
this.logger.info('Set Cookie ddf-auth');
res.cookie('ddf-auth', data.accessToken, {
domain: '.dadosfera.ai',
maxAge: exp,
httpOnly: true,
secure: true,
sameSite: 'none', // Necessário para cookies em requisições cross-site
});
}
if (data.refreshToken) {
this.logger.info('Set Cookie ddf-refresh-auth');
res.cookie('ddf-refresh-auth', data.refreshToken, {
domain: '.dadosfera.ai',
maxAge: exp,
httpOnly: true,
secure: true,
sameSite: 'none', // Necessário para cookies em requisições cross-site
});
}
if (data.userId) {
this.logger.info('Set Cookie ddf-refresh-auth');
res.cookie('ddf-user-id', data.userId, {
domain: '.dadosfera.ai',
maxAge: exp,
httpOnly: true,
secure: true,
sameSite: 'none', // Necessário para cookies em requisições cross-site
});
}
this.logger.info('Set session cookies');
writeSessionCookies(res, data);
}
/** Expires every session cookie this environment writes. */
public cleanUpAuthSession(res: Response) {
const exp = 1000 * 60 * 3;
res.cookie('ddf-auth', '', {
domain: 'dadosfera.ai',
maxAge: Date.now() - exp,
expires: new Date(),
httpOnly: true,
secure: true,
sameSite: 'none', // Necessário para cookies em requisições cross-site
});
res.cookie('ddf-refresh-auth', '', {
domain: 'dadosfera.ai',
maxAge: Date.now() - exp,
expires: new Date(),
httpOnly: true,
secure: true,
sameSite: 'none', // Necessário para cookies em requisições cross-site
});
clearSessionCookies(res);
this.logger.info('Clean cookie sessions');
}
@@ -432,9 +396,16 @@ export class AuthClientService implements OnModuleInit {
});
const { user } = await lastValueFrom(
this.userService.UserFindOneById({ id: payload.user_id }, metadata),
this.userService
.UserFindOneById({ id: payload.user_id }, metadata)
.pipe(timeout(authRpcTimeoutMs())),
);
if (!user) {
// Deleted/unknown user: a definitive "no session", not a crash.
throw new HttpException('User not found', HttpStatus.UNAUTHORIZED);
}
const userDto: UserDTO = {
id: user.id,
name: user.name,
@@ -447,6 +418,9 @@ export class AuthClientService implements OnModuleInit {
name: payload.customer_name,
tier: payload.customer_tier,
},
// Raw permission seqids from the JWT. Consumers own the seqid->meaning
// mapping (e.g. Orchest's auth-server); Maestro reports them as-is.
permissions: payload.permissions ?? [],
};
return userDto;
+2 -1
View File
@@ -152,5 +152,6 @@ export type UserDTO = {
id: string,
name: string,
tier: string,
}
},
permissions: number[],
}
+103
View File
@@ -0,0 +1,103 @@
import {
clearSessionCookies,
readAuthSession,
sessionCookieMaxAgeMs,
sessionCookieNames,
sessionCookieOptions,
writeSessionCookies,
} from './session-cookies';
const DAY_MS = 24 * 60 * 60 * 1000;
function fakeResponse() {
const cookie = jest.fn();
const clearCookie = jest.fn();
return { cookie, clearCookie };
}
describe('session cookies', () => {
it('uses the legacy names and domain when nothing is configured (production)', () => {
const env = {};
expect(sessionCookieNames(env)).toEqual({
accessToken: 'ddf-auth',
refreshToken: 'ddf-refresh-auth',
userId: 'ddf-user-id',
});
expect(sessionCookieOptions(env)).toMatchObject({
domain: '.dadosfera.ai',
path: '/',
httpOnly: true,
secure: true,
sameSite: 'none',
});
});
it('scopes names and domain per environment so staging never clobbers production', () => {
const env = { AUTH_COOKIE_SUFFIX: '-stg', COOKIE_DOMAIN: '.stg.dadosfera.ai' };
expect(sessionCookieNames(env)).toEqual({
accessToken: 'ddf-auth-stg',
refreshToken: 'ddf-refresh-auth-stg',
userId: 'ddf-user-id-stg',
});
expect(sessionCookieOptions(env).domain).toBe('.stg.dadosfera.ai');
});
it('treats maxAge as a duration (30 days by default), not as the token epoch', () => {
expect(sessionCookieMaxAgeMs({})).toBe(30 * DAY_MS);
expect(sessionCookieMaxAgeMs({ SESSION_COOKIE_MAX_AGE_DAYS: '7' })).toBe(7 * DAY_MS);
expect(sessionCookieMaxAgeMs({ SESSION_COOKIE_MAX_AGE_DAYS: 'nope' })).toBe(30 * DAY_MS);
expect(sessionCookieMaxAgeMs({ SESSION_COOKIE_MAX_AGE_DAYS: '-1' })).toBe(30 * DAY_MS);
});
it('reads the session using the environment-specific names', () => {
const env = { AUTH_COOKIE_SUFFIX: '-stg' };
const req = {
cookies: {
'ddf-auth': 'prod-token',
'ddf-auth-stg': 'stg-token',
'ddf-refresh-auth-stg': 'stg-refresh',
'ddf-user-id-stg': 'u-stg',
},
};
expect(readAuthSession(req as any, env)).toEqual({
accessToken: 'stg-token',
refreshToken: 'stg-refresh',
userId: 'u-stg',
});
expect(readAuthSession({ cookies: undefined } as any, env)).toEqual({
accessToken: undefined,
refreshToken: undefined,
userId: undefined,
});
});
it('writes the access-token cookie first and only the parts that are present', () => {
const res = fakeResponse();
writeSessionCookies(res as any, { accessToken: 'a', userId: 'u' }, {});
expect(res.cookie.mock.calls.map((c) => c[0])).toEqual(['ddf-auth', 'ddf-user-id']);
expect(res.cookie.mock.calls[0][2]).toMatchObject({
domain: '.dadosfera.ai',
path: '/',
maxAge: 30 * DAY_MS,
httpOnly: true,
secure: true,
sameSite: 'none',
});
});
it('clears all three cookies with the same domain and path they were set with', () => {
const res = fakeResponse();
const env = { AUTH_COOKIE_SUFFIX: '-stg', COOKIE_DOMAIN: '.stg.dadosfera.ai' };
clearSessionCookies(res as any, env);
expect(res.clearCookie.mock.calls.map((c) => c[0])).toEqual([
'ddf-auth-stg',
'ddf-refresh-auth-stg',
'ddf-user-id-stg',
]);
for (const call of res.clearCookie.mock.calls) {
expect(call[1]).toMatchObject({ domain: '.stg.dadosfera.ai', path: '/' });
}
});
});
+131
View File
@@ -0,0 +1,131 @@
import { CookieOptions, Request, Response } from 'express';
/**
* Browser session cookies written by Maestro and consumed by every app that
* authenticates through `GET /auth/me` (Orchest auth-server, nginx
* `auth_request` ingresses, ...).
*
* The names and the domain are environment-scoped so that a staging login
* never overwrites (or is confused with) a production session in the same
* browser: both platforms live under `dadosfera.ai`, so a cookie on the
* registrable domain is sent to *both* environments.
*
* ENV | AUTH_COOKIE_SUFFIX | COOKIE_DOMAIN
* prd | "" | .dadosfera.ai -> ddf-auth
* stg | "-stg" | .stg.dadosfera.ai -> ddf-auth-stg
*/
export type AuthSession = {
accessToken?: string;
refreshToken?: string;
userId?: string;
};
export type SessionCookieNames = {
accessToken: string;
refreshToken: string;
userId: string;
};
type Env = Record<string, string | undefined>;
const DEFAULT_COOKIE_DOMAIN = '.dadosfera.ai';
const DEFAULT_SESSION_COOKIE_MAX_AGE_DAYS = 30;
export function cookieDomain(env: Env = process.env): string {
return env.COOKIE_DOMAIN || DEFAULT_COOKIE_DOMAIN;
}
export function cookieSuffix(env: Env = process.env): string {
return env.AUTH_COOKIE_SUFFIX || '';
}
export function sessionCookieNames(env: Env = process.env): SessionCookieNames {
const suffix = cookieSuffix(env);
return {
accessToken: `ddf-auth${suffix}`,
refreshToken: `ddf-refresh-auth${suffix}`,
userId: `ddf-user-id${suffix}`,
};
}
/**
* How long the browser keeps the session cookies. This is a *duration*
* (express `maxAge` is in milliseconds from now), independent from the
* access token's own `exp`: an expired access token in the cookie is
* harmless — `/auth/me` transparently refreshes it — while a refresh token
* that outlives the cookie would make the browser forget a still-valid
* session. Keep it aligned with the IdP refresh-token validity.
*/
export function sessionCookieMaxAgeMs(env: Env = process.env): number {
const days = Number(env.SESSION_COOKIE_MAX_AGE_DAYS);
const safeDays =
Number.isFinite(days) && days > 0
? days
: DEFAULT_SESSION_COOKIE_MAX_AGE_DAYS;
return safeDays * 24 * 60 * 60 * 1000;
}
export function sessionCookieOptions(env: Env = process.env): CookieOptions {
return {
domain: cookieDomain(env),
path: '/',
httpOnly: true,
secure: true,
// Required: the cookies are sent cross-site (app.* -> maestro.*) and
// from embedded iframes (app.* embedding app-intelligence-*.*).
sameSite: 'none',
};
}
export function readAuthSession(
req: Pick<Request, 'cookies'>,
env: Env = process.env,
): AuthSession {
const names = sessionCookieNames(env);
const cookies = req.cookies ?? {};
return {
accessToken: cookies[names.accessToken] || undefined,
refreshToken: cookies[names.refreshToken] || undefined,
userId: cookies[names.userId] || undefined,
};
}
/**
* (Re)writes whichever parts of the session are present. The access-token
* cookie is written first on purpose: nginx `auth_request` relays only the
* first `Set-Cookie` of the auth subrequest (`$upstream_http_set_cookie`),
* and the access token is the one that changes on refresh.
*/
export function writeSessionCookies(
res: Pick<Response, 'cookie'>,
session: AuthSession,
env: Env = process.env,
): void {
const names = sessionCookieNames(env);
const options: CookieOptions = {
...sessionCookieOptions(env),
maxAge: sessionCookieMaxAgeMs(env),
};
if (session.accessToken) {
res.cookie(names.accessToken, session.accessToken, options);
}
if (session.refreshToken) {
res.cookie(names.refreshToken, session.refreshToken, options);
}
if (session.userId) {
res.cookie(names.userId, session.userId, options);
}
}
/** Expires all three session cookies (same name/domain/path they were set with). */
export function clearSessionCookies(
res: Pick<Response, 'clearCookie'>,
env: Env = process.env,
): void {
const names = sessionCookieNames(env);
const options = sessionCookieOptions(env);
res.clearCookie(names.accessToken, options);
res.clearCookie(names.refreshToken, options);
res.clearCookie(names.userId, options);
}
+54
View File
@@ -85,6 +85,9 @@ export class CatalogController {
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
)
@RequireModule(
DADOSFERA_MODULES_KEYS.CATALOG
)
async searchCatalog(
@User() user: RequestUser,
@Query() query: ICatalogAllRequest,
@@ -124,6 +127,9 @@ export class CatalogController {
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
)
@RequireModule(
DADOSFERA_MODULES_KEYS.CATALOG
)
async dowloadAsserts(
@User() user: RequestUser,
@Query() query: ICatalogAllRequest,
@@ -167,6 +173,9 @@ export class CatalogController {
@ApiInternalOnlyEndpoint()
@Get('data-asset')
@RequireModule(
DADOSFERA_MODULES_KEYS.CATALOG
)
async findByPipelineAndObject(@User() user: RequestUser, @Query() query) {
const { username, user_id, customer_id, customer_name, permissions } = user;
const { pipeline, object } = query;
@@ -225,6 +234,9 @@ export class CatalogController {
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
)
@RequireModule(
DADOSFERA_MODULES_KEYS.CATALOG
)
async findAllTags(@Body() body) {
this.logger.info(`/catalog - ON FIND ALL TAGS ROUTE`, {
user: body.info.user_id,
@@ -293,6 +305,9 @@ export class CatalogController {
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
)
@RequireModule(
DADOSFERA_MODULES_KEYS.CATALOG
)
async getDataAsset(
@User() user: RequestUser,
@Param('id') id: string,
@@ -404,6 +419,9 @@ export class CatalogController {
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
)
@RequireModule(
DADOSFERA_MODULES_KEYS.CATALOG
)
async getDataAssetColumnsMetadata(
@User() user: RequestUser,
@Language() language: LanguageEnum,
@@ -435,6 +453,9 @@ export class CatalogController {
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
)
@RequireModule(
DADOSFERA_MODULES_KEYS.CATALOG
)
async getDataAssetPreview(
@User() user: RequestUser,
@Language() language: LanguageEnum,
@@ -466,6 +487,9 @@ export class CatalogController {
PERMISSIONS_GROUPS.CATALOG.permissions.GET,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
)
@RequireModule(
DADOSFERA_MODULES_KEYS.CATALOG
)
async getDataAssetDocs(
@User() user: RequestUser,
@Language() language: LanguageEnum,
@@ -497,6 +521,9 @@ export class CatalogController {
PERMISSIONS_GROUPS.CATALOG.permissions.UPDATE,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
)
@RequireModule(
DADOSFERA_MODULES_KEYS.CATALOG
)
async updateDataAsset(
@User() user: RequestUser,
@Language() language: LanguageEnum,
@@ -532,6 +559,9 @@ export class CatalogController {
PERMISSIONS_GROUPS.CATALOG.permissions.CERTIFY,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
)
@RequireModule(
DADOSFERA_MODULES_KEYS.CATALOG
)
async updateDataAssetCertificationStatus(
@User() user: RequestUser,
@Language() language: LanguageEnum,
@@ -559,6 +589,9 @@ export class CatalogController {
PERMISSIONS_GROUPS.CATALOG.permissions.UPDATE,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
)
@RequireModule(
DADOSFERA_MODULES_KEYS.CATALOG
)
async manageDataAssetDocs(
@User() user: RequestUser,
@Headers() headers,
@@ -596,6 +629,9 @@ export class CatalogController {
@ApiInternalOnlyEndpoint()
@Put('data-asset/:id/manage-permissions')
@RequireModule(
DADOSFERA_MODULES_KEYS.CATALOG
)
async manageDataAssetPermissions(
@Param('id') id: string,
@User() user: RequestUser,
@@ -618,6 +654,9 @@ export class CatalogController {
@ApiInternalOnlyEndpoint()
@Put('data-asset/:id/revoke-permissions')
@RequireModule(
DADOSFERA_MODULES_KEYS.CATALOG
)
async revokeDataAssetPermissions(
@Param('id') id: string,
@User() user: RequestUser,
@@ -643,6 +682,9 @@ export class CatalogController {
PERMISSIONS_GROUPS.CATALOG.permissions.CREATE,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
)
@RequireModule(
DADOSFERA_MODULES_KEYS.CATALOG
)
async createDataAsset(
@User() user: RequestUser,
@Body() body: ICreateDataAsset,
@@ -667,6 +709,9 @@ export class CatalogController {
PERMISSIONS_GROUPS.CATALOG.permissions.UPDATE,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
)
@RequireModule(
DADOSFERA_MODULES_KEYS.CATALOG
)
async commentOnDataAsset(
@Param('id') id: string,
@User() user: RequestUser,
@@ -693,6 +738,9 @@ export class CatalogController {
PERMISSIONS_GROUPS.CATALOG.permissions.DELETE,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
)
@RequireModule(
DADOSFERA_MODULES_KEYS.CATALOG
)
async deleteDataAsset(@Param('id') id: string, @User() user: RequestUser) {
const { customer_id, customer_name, user_id, username } = user;
const metadata = PackTheMetadata({
@@ -714,6 +762,9 @@ export class CatalogController {
PERMISSIONS_GROUPS.CATALOG.permissions.UPDATE,
PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER,
)
@RequireModule(
DADOSFERA_MODULES_KEYS.CATALOG
)
async deleteComment(
@Param('id') id: string,
@User() user: RequestUser,
@@ -867,6 +918,9 @@ export class CatalogController {
@Get('nimbus-dashboards')
@RequireAllPermissions(PERMISSIONS_GROUPS.CATALOG.permissions.DATA_MANAGER)
@RequireModule(
DADOSFERA_MODULES_KEYS.CATALOG
)
async getNimbusDashboards(
@User() user: RequestUser,
@Body() body: GetNimbusDashboardsRequest,
@@ -27,15 +27,19 @@ import {
RefreshCatalogStatusReq,
} from './dto/connection-test';
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import { Authenticated } from 'src/decorators/authentication.decorator';
import { Authenticated, RequireModule } from 'src/decorators/authentication.decorator';
import { GrpcToHttpExceptionFilter } from 'src/error/grpc-to-http-exception.filter';
import { ApiInternalOnlyController } from 'src/decorators/swagger.decorator';
import { DADOSFERA_MODULES_KEYS } from 'src/authentication/permissions.enum';
@ApiInternalOnlyController()
@ApiTags('Connection Test')
@Controller('connection-test')
@UseFilters(new GrpcToHttpExceptionFilter())
@Authenticated()
@RequireModule(
DADOSFERA_MODULES_KEYS.COLLECT
)
export class ConnectionTestController {
logger: any;
constructor(
@@ -16,8 +16,9 @@ import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import {
Authenticated,
RequireAllPermissions,
RequireModule,
} from 'src/decorators/authentication.decorator';
import { PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
import { DADOSFERA_MODULES_KEYS, PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
import { RequestUser, User } from 'src/decorators/user.decorator';
import { ValidationPipe } from '../../pipes/object-validation.pipe';
import {
@@ -39,6 +40,9 @@ const connectionPermissions = PERMISSIONS_GROUPS.CONNECTION.permissions;
@ApiTags('connections')
@Authenticated()
@Controller('connections')
@RequireModule(
DADOSFERA_MODULES_KEYS.COLLECT
)
export class ConnectionController {
logger: any;
constructor(
+26 -1
View File
@@ -25,9 +25,10 @@ import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import {
Authenticated,
RequireAllPermissions,
RequireModule,
RequireSomePermission,
} from 'src/decorators/authentication.decorator';
import { PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
import { DADOSFERA_MODULES_KEYS, PERMISSIONS_GROUPS } from 'src/authentication/permissions.enum';
import { Language } from 'src/decorators/language.decorator';
import { LanguageEnum } from 'src/utils/languages.enum';
import { ApiInternalOnlyController } from 'src/decorators/swagger.decorator';
@@ -99,6 +100,9 @@ export class ConnectorController {
PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE,
PERMISSIONS_GROUPS.PIPELINE.permissions.DELETE,
)
@RequireModule(
DADOSFERA_MODULES_KEYS.COLLECT
)
async getAllConnectors(
@Language() language: LanguageEnum,
@Query() queries: GetAllDto,
@@ -131,6 +135,9 @@ export class ConnectorController {
PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE,
PERMISSIONS_GROUPS.PIPELINE.permissions.DELETE,
)
@RequireModule(
DADOSFERA_MODULES_KEYS.COLLECT
)
async getConnectorsTags() {
return await this.connectorClientService.getConnectorsTags();
}
@@ -143,6 +150,9 @@ export class ConnectorController {
PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE,
PERMISSIONS_GROUPS.PIPELINE.permissions.DELETE,
)
@RequireModule(
DADOSFERA_MODULES_KEYS.COLLECT
)
async getConnector(
@Language() language: LanguageEnum,
@Param('plugin') plugin: string,
@@ -171,6 +181,9 @@ export class ConnectorController {
PERMISSIONS_GROUPS.PIPELINE.permissions.UPDATE,
PERMISSIONS_GROUPS.PIPELINE.permissions.DELETE,
)
@RequireModule(
DADOSFERA_MODULES_KEYS.COLLECT
)
async getConnectorDetails(
@Language() language: LanguageEnum,
@Param('plugin') plugin: string,
@@ -193,6 +206,9 @@ export class ConnectorController {
@Put('/:plugin')
@RequireAllPermissions(PERMISSIONS_GROUPS.CONNECTORS.permissions.UPDATE)
@ApiConsumes('multipart/form-data')
@RequireModule(
DADOSFERA_MODULES_KEYS.COLLECT
)
async updateConnector(
@Param('plugin') plugin: string,
@Body() body: UpdateDto,
@@ -214,6 +230,9 @@ export class ConnectorController {
@Put('/:plugin/add-tag')
@RequireAllPermissions(PERMISSIONS_GROUPS.CONNECTORS.permissions.UPDATE)
@RequireModule(
DADOSFERA_MODULES_KEYS.COLLECT
)
async addTagOnConnector(
@Param('plugin') plugin: string,
@Body() body: AddTagDto,
@@ -241,6 +260,9 @@ export class ConnectorController {
@Put('/:plugin/remove-tag')
@RequireAllPermissions(PERMISSIONS_GROUPS.CONNECTORS.permissions.UPDATE)
@RequireModule(
DADOSFERA_MODULES_KEYS.COLLECT
)
async removeTagOnConnector(
@Param('plugin') plugin: string,
@Body() body: RemoveTagDto,
@@ -269,6 +291,9 @@ export class ConnectorController {
@Delete('/:plugin')
@RequireAllPermissions(PERMISSIONS_GROUPS.CONNECTORS.permissions.DELETE)
@RequireModule(
DADOSFERA_MODULES_KEYS.COLLECT
)
async deleteConnector(
@Param('plugin') plugin: string,
@Query('version') version: string,
@@ -1,6 +1,6 @@
import { ClientsModule } from '@nestjs/microservices';
import { Test, TestingModule } from '@nestjs/testing';
// import { DucClient } from 'src/clients/duc/client.config';
// import { DucClient } from '../duc/client.config'
import { PermissionsController } from './permissions.controller';
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import { PermissionsService } from './permissions.service';
@@ -22,7 +22,7 @@ describe.skip('PermissionsController', () => {
provide: DadosferaLogger,
useValue: { logger },
},
PermissionsService,
PermissionsService
],
}).compile();
@@ -25,9 +25,10 @@ import {
} from '@nestjs/swagger';
import {
RequireAllPermissions,
RequireModule,
RequireSomePermission,
} from 'src/decorators/authentication.decorator';
import { PERMISSIONS_GROUPS } from '../../authentication/permissions.enum';
import { DADOSFERA_MODULES_KEYS, PERMISSIONS_GROUPS } from '../../authentication/permissions.enum';
import { PipelinesService } from './pipelines.service';
import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import { Messages } from '@dadosfera/protospack-v2/dist/lib/PipelineV2';
@@ -57,6 +58,9 @@ type PipelineTablesConfig = { input_id?: string; tables: PipelineTable[] };
@ApiHeaders([{ name: 'dadosfera-lang', enum: LanguageEnum, required: false }])
@UseFilters(new GrpcToHttpExceptionFilter())
@Controller('pipelinesV2')
@RequireModule(
DADOSFERA_MODULES_KEYS.COLLECT
)
export class PipelinesController {
logger: DadosferaLogger;
constructor(
@@ -20,10 +20,11 @@ import { DadosferaLogger } from '@dadosfera/dadosfera-logs';
import {
Authenticated,
RequireAllPermissions,
RequireModule,
} from '../../decorators/authentication.decorator';
import { User, RequestUser } from '../../decorators/user.decorator';
import { PlatformApiService } from './platform-api.service';
import { PERMISSIONS_GROUPS } from '../../authentication/permissions.enum';
import { DADOSFERA_MODULES_KEYS, PERMISSIONS_GROUPS } from '../../authentication/permissions.enum';
import { ElasticsearchService } from '../../services/elasticsearch';
import { DynamoDBService, ReferenceColumn } from '../../services/dynamodb';
import { CustomersService } from '../customers/customers.service';
@@ -49,6 +50,7 @@ type RenameTablesBody = {
@ApiTags('Platform API')
@Controller('platform')
@RequireModule(DADOSFERA_MODULES_KEYS.COLLECT)
export class PlatformApiController {
private logger: any;
@@ -1,14 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ReleaseNoteController } from './release_note.controller';
import { ReleaseNoteService } from './release_note.service';
import DadosferaLogger from '@dadosfera/dadosfera-logs';
describe('ReleaseNoteController', () => {
let controller: ReleaseNoteController;
const logger = {
info: (...args) => args,
error: (...args) => args,
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ReleaseNoteController],
providers: [ReleaseNoteService],
providers: [
{
provide: DadosferaLogger,
useValue: { logger },
},
ReleaseNoteService
],
}).compile();
controller = module.get<ReleaseNoteController>(ReleaseNoteController);
@@ -1,12 +1,23 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ReleaseNoteService } from './release_note.service';
import DadosferaLogger from '@dadosfera/dadosfera-logs';
describe('ReleaseNoteService', () => {
let service: ReleaseNoteService;
const logger = {
info: (...args) => args,
error: (...args) => args,
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ReleaseNoteService],
providers: [
{
provide: DadosferaLogger,
useValue: { logger },
},
ReleaseNoteService
],
}).compile();
service = module.get<ReleaseNoteService>(ReleaseNoteService);
+1 -1
View File
@@ -1,4 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts", "jest.config.ts"]
"exclude": ["node_modules", "test", "dist", "**/*spec.ts", "jest.*.ts"]
}