diff --git a/MEETING_SCHEDULE.md b/MEETING_SCHEDULE.md index 671da36fa1..695a19855f 100644 --- a/MEETING_SCHEDULE.md +++ b/MEETING_SCHEDULE.md @@ -3,6 +3,34 @@ Dear community! We're excited to announce bi-weekly office hours. +## #5 Q&A + +Dear community, + +This week's office hour is dedicated for you to drop by and ask any questions you may have about ZITADEL. We are happy to discuss anything, from Actions to Zero downtime deployments. + +Join us on the stage or ask your questions in the chat next Wednesday in the office hours channel on Discord. We're looking forward to have a nice chat with you. + +**What to expect:** + +* **Q&A Session**: Ask your questions and feel free to join the discussion to help others getting their questions answered + +**Details:** + +* **Target Audience:** Developers and IT Ops personnel using ZITADEL +* **Topic:** Q\&A session +* **When**: Wednesday 25th of September 6 pm UTC +* **Duration**: about 1 hour +* **Platform:** Zitadel Discord Server (Join us here: https://discord.gg/zitadel-927474939156643850?event=1286221582838272000 ) + +**In the meantime:** + +If you have questions upfront, feel free to already post them in the chat of the [office hours channel](https://zitadel.com/office-hours) on our Discord server :gigi: + +We look forward to seeing you there\! + +**P.S.** Spread the word\! Share this announcement with your fellow ZITADEL users who might be interested 📢 + ## #4 Login UI deepdive Dear community, diff --git a/console/package.json b/console/package.json index 1631a206d6..e68ccec202 100644 --- a/console/package.json +++ b/console/package.json @@ -28,7 +28,7 @@ "@fortawesome/angular-fontawesome": "^0.13.0", "@fortawesome/fontawesome-svg-core": "^6.4.2", "@fortawesome/free-brands-svg-icons": "^6.4.2", - "@grpc/grpc-js": "^1.11.1", + "@grpc/grpc-js": "^1.11.2", "@netlify/framework-info": "^9.8.13", "@ngx-translate/core": "^15.0.0", "angular-oauth2-oidc": "^15.0.1", @@ -42,14 +42,14 @@ "google-protobuf": "^3.21.2", "grpc-web": "^1.4.1", "i18n-iso-countries": "^7.7.0", - "libphonenumber-js": "^1.11.4", + "libphonenumber-js": "^1.11.8", "material-design-icons-iconfont": "^6.1.1", "moment": "^2.29.4", "ngx-color": "^9.0.0", "opentype.js": "^1.3.4", "rxjs": "~7.8.0", "tinycolor2": "^1.6.0", - "tslib": "^2.6.2", + "tslib": "^2.7.0", "uuid": "^10.0.0", "zone.js": "~0.13.3" }, @@ -60,16 +60,16 @@ "@angular-eslint/eslint-plugin-template": "18.0.0", "@angular-eslint/schematics": "16.2.0", "@angular-eslint/template-parser": "18.3.0", - "@angular/cli": "^16.2.14", + "@angular/cli": "^16.2.15", "@angular/compiler-cli": "^16.2.5", - "@angular/language-service": "^18.2.2", - "@bufbuild/buf": "^1.39.0", + "@angular/language-service": "^18.2.4", + "@bufbuild/buf": "^1.41.0", "@types/file-saver": "^2.0.7", "@types/google-protobuf": "^3.15.3", "@types/jasmine": "~5.1.4", "@types/jasminewd2": "~2.0.13", "@types/jsonwebtoken": "^9.0.6", - "@types/node": "^22.5.2", + "@types/node": "^22.5.5", "@types/opentype.js": "^1.3.8", "@types/qrcode": "^1.5.2", "@types/uuid": "^10.0.0", @@ -77,7 +77,7 @@ "@typescript-eslint/parser": "^5.60.1", "codelyzer": "^6.0.2", "eslint": "^8.50.0", - "jasmine-core": "~5.2.0", + "jasmine-core": "~5.3.0", "jasmine-spec-reporter": "~7.0.0", "karma": "^6.4.2", "karma-chrome-launcher": "^3.2.0", diff --git a/console/src/app/modules/user-grants/user-grants.component.html b/console/src/app/modules/user-grants/user-grants.component.html index efca9f74ec..82de67c8bb 100644 --- a/console/src/app/modules/user-grants/user-grants.component.html +++ b/console/src/app/modules/user-grants/user-grants.component.html @@ -154,10 +154,25 @@ + + {{ 'PROJECT.GRANT.STATE' | translate }} + + + {{ 'USER.DATA.STATE' + grant.state | translate }} + + + + - + diff --git a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html index 45b6821b4b..9e319b7bc6 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html +++ b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.html @@ -222,7 +222,7 @@ diff --git a/console/yarn.lock b/console/yarn.lock index eec0cc2438..9b87fccf03 100644 --- a/console/yarn.lock +++ b/console/yarn.lock @@ -26,6 +26,14 @@ "@angular-devkit/core" "16.2.14" rxjs "7.8.1" +"@angular-devkit/architect@0.1602.15": + version "0.1602.15" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1602.15.tgz#b70f2456677f6859d4dac4ad80c6b13d00108797" + integrity sha512-+yPlUG5c8l7Z/A6dyeV7NQjj4WDWnWWQt+8eW/KInwVwoYiM32ntTJ0M4uU/aDdHuwKQnMLly28AcSWPWKYf2Q== + dependencies: + "@angular-devkit/core" "16.2.15" + rxjs "7.8.1" + "@angular-devkit/build-angular@^16.2.2": version "16.2.14" resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-16.2.14.tgz#0c4e41aa3f67e52b474b2fabeb027aebf6e76566" @@ -118,12 +126,24 @@ rxjs "7.8.1" source-map "0.7.4" -"@angular-devkit/schematics@16.2.14": - version "16.2.14" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.2.14.tgz#819c2ef8bb298e383cb312d9d1411f5970f0328f" - integrity sha512-B6LQKInCT8w5zx5Pbroext5eFFRTCJdTwHN8GhcVS8IeKCnkeqVTQLjB4lBUg7LEm8Y7UHXwzrVxmk+f+MBXhw== +"@angular-devkit/core@16.2.15": + version "16.2.15" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.2.15.tgz#44ef98cda82ef82435a2a41507f8c24720d372df" + integrity sha512-68BgPWpcjNKz++uvLFG8IZaOH3ti2BWQVqaE3yTIYaMoNt0y0A0X2MUVd7EGbAGUk2JdloWJv5LTPVZMzCuK4w== dependencies: - "@angular-devkit/core" "16.2.14" + ajv "8.12.0" + ajv-formats "2.1.1" + jsonc-parser "3.2.0" + picomatch "2.3.1" + rxjs "7.8.1" + source-map "0.7.4" + +"@angular-devkit/schematics@16.2.15": + version "16.2.15" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.2.15.tgz#cedcb48fdd240db0a779674cf52455a78a4098bb" + integrity sha512-C/j2EwapdBMf1HWDuH89bA9B2e511iEYImkyZ+vCSXRwGiWUaZCrhl18bvztpErTrdOLM3mCwNXWEAMXI4zUXA== + dependencies: + "@angular-devkit/core" "16.2.15" jsonc-parser "3.2.0" magic-string "0.30.1" ora "5.4.1" @@ -242,15 +262,15 @@ optionalDependencies: parse5 "^7.1.2" -"@angular/cli@^16.2.14": - version "16.2.14" - resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-16.2.14.tgz#ab58910ae354ee31b89a7479efd5978fd1a3042e" - integrity sha512-0y71jtitigVolm4Rim1b8xPQ+B22cGp4Spef2Wunpqj67UowN6tsZaVuWBEQh4u5xauX8LAHKqsvy37ZPWCc4A== +"@angular/cli@^16.2.15": + version "16.2.15" + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-16.2.15.tgz#951d84ef9a7113242b10fe89be1adfa3a94dd6aa" + integrity sha512-nNUmt0ZRj2xHH8tGXSJUiusP5rmakAz0f6cc6T4p03OyeShOKdvs9+/F4hzzsM79/ylZofBlFfwYVCBTbOtMqw== dependencies: - "@angular-devkit/architect" "0.1602.14" - "@angular-devkit/core" "16.2.14" - "@angular-devkit/schematics" "16.2.14" - "@schematics/angular" "16.2.14" + "@angular-devkit/architect" "0.1602.15" + "@angular-devkit/core" "16.2.15" + "@angular-devkit/schematics" "16.2.15" + "@schematics/angular" "16.2.15" "@yarnpkg/lockfile" "1.1.0" ansi-colors "4.1.3" ini "4.1.1" @@ -318,10 +338,10 @@ dependencies: tslib "^2.3.0" -"@angular/language-service@^18.2.2": - version "18.2.2" - resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-18.2.2.tgz#8a6b3f224871cb4b1dd5d76a43a1c3884d14aa62" - integrity sha512-aROQNQeLf+o+F5OVvE/9BUe/Tpv8pjzmrZlogBbic5cb4IqSNhR4RjxbgIyXBO/6bhLCZwqfmMqRbW2J2xqMkg== +"@angular/language-service@^18.2.4": + version "18.2.4" + resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-18.2.4.tgz#c449a75bc405bf519fc90f7a9269a98e2a1f7758" + integrity sha512-Keg6n8u8xHLhRDTmx4hUqh1AtVFUt8hDxPMYSUu64czjOT5Dnh8XsgKagu563NEjxbDaCzttPuO+y3DlcaDZoQ== "@angular/material-moment-adapter@^16.2.4": version "16.2.14" @@ -1462,47 +1482,47 @@ "@babel/helper-validator-identifier" "^7.24.7" to-fast-properties "^2.0.0" -"@bufbuild/buf-darwin-arm64@1.39.0": - version "1.39.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.39.0.tgz#0ab8453dc7fc7694e5bd39c69d934edc51b81c81" - integrity sha512-Ptl0uAGssLxQTzoZhGwv1FFTbzUfcstIpEwMhN+XrwiuqsSxOg9eq/n3yXoci5VJsHokjDUHnWkR3y+j5P/5KA== +"@bufbuild/buf-darwin-arm64@1.41.0": + version "1.41.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.41.0.tgz#a6aee96452f5a624eb7e5b0336833fdd7a3a7911" + integrity sha512-+G5DwpIgnm0AkqgxORxoYXVT0RGDcw8P4SXFXcovgvDBkk9rPvEI1dbPF83n3SUxzcu2A2OxC7DxlXszWIh2Gw== -"@bufbuild/buf-darwin-x64@1.39.0": - version "1.39.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.39.0.tgz#9c9a211c8039b8cb89b45bf44f338edf82d5e506" - integrity sha512-XNCuy9sjQwVJ4NIZqxaTIyzUtlyquSkp/Uuoh5W5thJ3nzZ5RSgvXKF5iXHhZmesrfRGApktwoCx5Am8runsfQ== +"@bufbuild/buf-darwin-x64@1.41.0": + version "1.41.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.41.0.tgz#aac6a6b86f6d1f30c86f70e918d212e067e5257f" + integrity sha512-qjkJ/LAWqNk3HX65n+JTt18WtKrhrrAhIu3Dpfbe0eujsxafFZKoPzlWJYybxvsaF9CdEyMMm/OalBPpoosMOA== -"@bufbuild/buf-linux-aarch64@1.39.0": - version "1.39.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.39.0.tgz#9778732efbdbbfe02ec821017cc2392ce4a0153f" - integrity sha512-Am+hrw94awp/eY027ROXwRQBuwAzOpQ/4zI4dgmgsyhzeWZ8w1LWC8z2SSr8T2cqd0cm52KxtoWMW+B3b2qzbw== +"@bufbuild/buf-linux-aarch64@1.41.0": + version "1.41.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.41.0.tgz#8ac97e7a19cf0c0957ca1b3e690d8c039b0b3468" + integrity sha512-5E+MLAF4QHPwAjwVVRRP3Is2U3zpIpQQR7S3di9HlKACbgvefJEBrUfRqQZvHrMuuynQRqjFuZD16Sfvxn9rCQ== -"@bufbuild/buf-linux-x64@1.39.0": - version "1.39.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.39.0.tgz#d7ca62c4f506c60011f5a97ca2e8683aa26693b0" - integrity sha512-JXVkHoMrTvmpseqdoQPJJ6MRV7/vlloYtvXHHACEzVytYjljOYCNoVET/E5gLBco/edeXFMNc40cCi1KgL3rSw== +"@bufbuild/buf-linux-x64@1.41.0": + version "1.41.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.41.0.tgz#8a272846929215affccb9c271f02948e10f8d4a9" + integrity sha512-W4T+uqmdtypzzatv6OXjUzGacZiNzGECogr+qDkJF38MSZd3jHXhTEN2KhRckl3i9rRAnfHBwG68BjCTxxBCOQ== -"@bufbuild/buf-win32-arm64@1.39.0": - version "1.39.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.39.0.tgz#efdaf1eca30445f04124c6d829a46a676e6b1dc3" - integrity sha512-akdGW02mo04wbLfjNMBQqxC4mPQ/L/vTU8/o79I67GSxyFYt7bKifvYIYhAA39C2gibHyB7ZLmoeRPbaU8wbYA== +"@bufbuild/buf-win32-arm64@1.41.0": + version "1.41.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.41.0.tgz#e26b67b2da15e284326c3d3c38255b443e201e0b" + integrity sha512-OsRVoTZHJZYGIphAwaRqcCeYR9Sk5VEMjpCJiFt/dkHxx2acKH4u/7O+633gcCxQL8EnsU2l8AfdbW7sQaOvlg== -"@bufbuild/buf-win32-x64@1.39.0": - version "1.39.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.39.0.tgz#09f2b0290818d826847689d6149f8fb0def4ac4b" - integrity sha512-jos08UMg9iUZsGjPrNpLXP+FNk6q6GizO+bjee/GcI0kSijIzXYMg14goQr0TKlvqs/+IRAM5vZIokQBYlAENQ== +"@bufbuild/buf-win32-x64@1.41.0": + version "1.41.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.41.0.tgz#b2ff4e9cdb9f73baaad216d35c91c733c4c4b661" + integrity sha512-2KJLp7Py0GsfRjDxwBzS17RMpaYFGCvzkwY5CtxfPMw8cg6cE7E36r+vcjHh5dBOj/CumaiXLTwxhCSBtp0V1g== -"@bufbuild/buf@^1.39.0": - version "1.39.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf/-/buf-1.39.0.tgz#65884f55d072b93122959c92b389c1d7d8ab510b" - integrity sha512-lm7xb9pc7X04rRjCQ69o9byAAZ7/dsUQGoH+iJ9uBSXQWiwQ1Ts8gneBnuUVsAH2vdW73NFBpmNQGE9XtFauVQ== +"@bufbuild/buf@^1.41.0": + version "1.41.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf/-/buf-1.41.0.tgz#76077338696009c2f34e7ca1c76baf89a04079f5" + integrity sha512-6pN2fqMrPqnIkrC1q9KpXpu7fv3Rul0ZPhT4MSYYj+8VcyR3kbLVk6K+CzzPvYhr4itfotnI3ZVGQ/X/vupECg== optionalDependencies: - "@bufbuild/buf-darwin-arm64" "1.39.0" - "@bufbuild/buf-darwin-x64" "1.39.0" - "@bufbuild/buf-linux-aarch64" "1.39.0" - "@bufbuild/buf-linux-x64" "1.39.0" - "@bufbuild/buf-win32-arm64" "1.39.0" - "@bufbuild/buf-win32-x64" "1.39.0" + "@bufbuild/buf-darwin-arm64" "1.41.0" + "@bufbuild/buf-darwin-x64" "1.41.0" + "@bufbuild/buf-linux-aarch64" "1.41.0" + "@bufbuild/buf-linux-x64" "1.41.0" + "@bufbuild/buf-win32-arm64" "1.41.0" + "@bufbuild/buf-win32-x64" "1.41.0" "@colors/colors@1.5.0": version "1.5.0" @@ -1810,10 +1830,10 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== -"@grpc/grpc-js@^1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.11.1.tgz#a92f33e98f1959feffcd1b25a33b113d2c977b70" - integrity sha512-gyt/WayZrVPH2w/UTLansS7F9Nwld472JxxaETamrM8HNlsa+jSLNyKAZmhxI2Me4c3mQHFiS1wWHDY1g1Kthw== +"@grpc/grpc-js@^1.11.2": + version "1.11.2" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.11.2.tgz#541a00303e533b5efe9d84ed61b84cdf9dd93ee7" + integrity sha512-DWp92gDD7/Qkj7r8kus6/HCINeo3yPZWZ3paKgDgsbKbSpoxKg1yvN8xe2Q8uE3zOsPe3bX8FQX2+XValq2yTw== dependencies: "@grpc/proto-loader" "^0.7.13" "@js-sdsl/ordered-map" "^4.4.2" @@ -2884,13 +2904,13 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== -"@schematics/angular@16.2.14": - version "16.2.14" - resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-16.2.14.tgz#3aac7e05b6e3919195275cf06ac403d7a3567876" - integrity sha512-YqIv727l9Qze8/OL6H9mBHc2jVXzAGRNBYnxYWqWhLbfvuVbbldo6NNIIjgv6lrl2LJSdPAAMNOD5m/f6210ug== +"@schematics/angular@16.2.15": + version "16.2.15" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-16.2.15.tgz#f3b810842959808f0d65ce816f4f0c1a7463c176" + integrity sha512-T7wEGYxidpLAkis+hO5nsVfnWsy6sXf1T9GS8uztC8IYYsnqB9jTVfjVyfhASugZasdmx7+jWv3oCGy6Z5ZehA== dependencies: - "@angular-devkit/core" "16.2.14" - "@angular-devkit/schematics" "16.2.14" + "@angular-devkit/core" "16.2.15" + "@angular-devkit/schematics" "16.2.15" jsonc-parser "3.2.0" "@sigstore/bundle@^1.1.0": @@ -3098,10 +3118,10 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=13.7.0", "@types/node@^22.5.2": - version "22.5.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.2.tgz#e42344429702e69e28c839a7e16a8262a8086793" - integrity sha512-acJsPTEqYqulZS/Yp/S3GgeE6GZ0qYODUR8aVr/DkhHQ8l9nd4j5x1/ZJy9/gHrRlFMqkO6i0I3E27Alu4jjPg== +"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=13.7.0", "@types/node@^22.5.5": + version "22.5.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.5.tgz#52f939dd0f65fc552a4ad0b392f3c466cc5d7a44" + integrity sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA== dependencies: undici-types "~6.19.2" @@ -3978,10 +3998,10 @@ blocking-proxy@^1.0.0: dependencies: minimist "^1.2.0" -body-parser@1.20.2, body-parser@^1.19.0: - version "1.20.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" - integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== +body-parser@1.20.3, body-parser@^1.19.0: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== dependencies: bytes "3.1.2" content-type "~1.0.5" @@ -3991,7 +4011,7 @@ body-parser@1.20.2, body-parser@^1.19.0: http-errors "2.0.0" iconv-lite "0.4.24" on-finished "2.4.1" - qs "6.11.0" + qs "6.13.0" raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" @@ -4902,6 +4922,11 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + encoding@^0.1.13: version "0.1.13" resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" @@ -5276,36 +5301,36 @@ exponential-backoff@^3.1.1: integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== express@^4.17.3: - version "4.19.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" - integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + version "4.20.0" + resolved "https://registry.yarnpkg.com/express/-/express-4.20.0.tgz#f1d08e591fcec770c07be4767af8eb9bcfd67c48" + integrity sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.2" + body-parser "1.20.3" content-disposition "0.5.4" content-type "~1.0.4" cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" etag "~1.8.1" finalhandler "1.2.0" fresh "0.5.2" http-errors "2.0.0" - merge-descriptors "1.0.1" + merge-descriptors "1.0.3" methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.7" + path-to-regexp "0.1.10" proxy-addr "~2.0.7" qs "6.11.0" range-parser "~1.2.1" safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" + send "0.19.0" + serve-static "1.16.0" setprototypeof "1.2.0" statuses "2.0.1" type-is "~1.6.18" @@ -6482,10 +6507,10 @@ jasmine-core@~2.8.0: resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" integrity sha512-SNkOkS+/jMZvLhuSx1fjhcNWUC/KG6oVyFUGkSBEr9n1axSNduWU8GlI7suaHXr4yxjet6KjrUZxUTE5WzzWwQ== -jasmine-core@~5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-5.2.0.tgz#7d0aa4c26cb3dbaed201d0505489baf1e48faeca" - integrity sha512-tSAtdrvWybZkQmmaIoDgnvHG8ORUNw5kEVlO5CvrXj02Jjr9TZrmjFq7FUiOUzJiOP2wLGYT6PgrQgQF4R1xiw== +jasmine-core@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-5.3.0.tgz#ed784e5a10af43988d8408bad80b9f08e240a3f8" + integrity sha512-zsOmeBKESky4toybvWEikRiZ0jHoBEu79wNArLfMdSnlLMZx3Xcp6CSm2sUcYyoJC+Uyj8LBJap/MUbVSfJ27g== jasmine-spec-reporter@~7.0.0: version "7.0.0" @@ -6810,10 +6835,10 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -libphonenumber-js@^1.11.4: - version "1.11.5" - resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.5.tgz#50a441da5ff9ed9a322d796a14f1e9cbc0fdabdf" - integrity sha512-TwHR5BZxGRODtAfz03szucAkjT5OArXr+94SMtAM2pYXIlQNVMrxvb6uSCbnaJJV6QXEyICk7+l6QPgn72WHhg== +libphonenumber-js@^1.11.8: + version "1.11.8" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.8.tgz#697fdd36500a97bc672d7927d867edf34b4bd2a7" + integrity sha512-0fv/YKpJBAgXKy0kaS3fnqoUVN8901vUYAKIGD/MWZaDfhJt1nZjPL3ZzdZBt/G8G8Hw2J1xOIrXWdNHFHPAvg== license-webpack-plugin@4.0.2: version "4.0.2" @@ -7034,10 +7059,10 @@ memfs@^3.4.12, memfs@^3.4.3: dependencies: fs-monkey "^1.0.4" -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== merge-stream@^2.0.0: version "2.0.0" @@ -7855,10 +7880,10 @@ path-scurry@^1.11.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== path-type@^4.0.0: version "4.0.0" @@ -8145,6 +8170,13 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + qs@~6.5.2: version "6.5.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" @@ -8618,6 +8650,25 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" @@ -8638,10 +8689,10 @@ serve-index@^1.9.1: mime-types "~2.1.17" parseurl "~1.3.2" -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== +serve-static@1.16.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.0.tgz#2bf4ed49f8af311b519c46f272bf6ac3baf38a92" + integrity sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA== dependencies: encodeurl "~1.0.2" escape-html "~1.0.3" @@ -8704,7 +8755,7 @@ shell-quote@^1.8.1: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== -side-channel@^1.0.4: +side-channel@^1.0.4, side-channel@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== @@ -8956,7 +9007,16 @@ streamroller@^3.1.5: debug "^4.3.4" fs-extra "^8.1.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8993,7 +9053,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -9007,6 +9067,13 @@ strip-ansi@^3.0.0: dependencies: ansi-regex "^2.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -9269,10 +9336,10 @@ tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" - integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== tsutils@^3.21.0: version "3.21.0" @@ -9781,7 +9848,7 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -9799,6 +9866,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" diff --git a/docs/docs/examples/imports/_setup_pylon.mdx b/docs/docs/examples/imports/_setup_pylon.mdx deleted file mode 100644 index e68dad2c3c..0000000000 --- a/docs/docs/examples/imports/_setup_pylon.mdx +++ /dev/null @@ -1 +0,0 @@ -You have to install Pylon as described in [their documentation](https://pylon.cronit.io/docs/installation/). diff --git a/docs/docs/examples/secure-api/pylon.mdx b/docs/docs/examples/secure-api/pylon.mdx index 81f66999af..c8a3cd4635 100644 --- a/docs/docs/examples/secure-api/pylon.mdx +++ b/docs/docs/examples/secure-api/pylon.mdx @@ -6,7 +6,6 @@ sidebar_label: Pylon import AppJWT from "../imports/_app_jwt.mdx"; import ServiceuserJWT from "../imports/_serviceuser_jwt.mdx"; import ServiceuserRole from "../imports/_serviceuser_role.mdx"; -import SetupPylon from "../imports/_setup_pylon.mdx"; This integration guide demonstrates the recommended way to incorporate ZITADEL into your [Pylon](https://pylon.cronit.io) service. It explains how to check the token validity in the API and how to check for permissions. @@ -43,26 +42,27 @@ And the following from the Serviceuser: ## Setup new Pylon service -### Setup Pylon +Pylon allows you to create a new service using the `npm create pylon` command. This command creates a new Pylon project with a basic project structure and configuration. +During the setup process, you can choose your preferred runtime, such as Bun, Node.js, or Cloudflare Workers. - +**This guide uses the Bun runtime.** ### Creating a new project To create a new Pylon project, run the following command: ```bash -pylon new my-pylon-project +npm create pylon my-pylon@latest ``` -This will create a new directory called `my-pylon-project` with a basic Pylon project structure. +This will create a new directory called `my-pylon` with a basic Pylon project structure. ### Project structure Pylon projects are structured as follows: ``` -my-pylon-project/ +my-pylon/ ├── .pylon/ ├── src/ │ ├── index.ts @@ -81,16 +81,18 @@ my-pylon-project/ Here's an example of a basic Pylon service: ```ts -import { defineService } from "@getcronit/pylon"; +import { app } from "@getcronit/pylon"; -export default defineService({ +export const graphql = { Query: { sum: (a: number, b: number) => a + b, }, Mutation: { divide: (a: number, b: number) => a / b, }, -}); +}; + +export default app; ``` ## Secure the API @@ -113,6 +115,8 @@ AUTH_PROJECT_ID='250719519163548112' 2. Copy the `.json`-key-file that you downloaded from the ZITADEL Console into the root folder of your project and rename it to `key.json`. +3. (Optional) For added convenience in production environments, you can include the content of the .json key file as `AUTH_KEY` in the .env file or as an environment variable. + ### Auth Pylon provides a auth module and a decorator to check the validity of the token and the permissions. @@ -140,8 +144,7 @@ The following code demonstrates how to create a Pylon service with the required ```ts import { - defineService, - PylonAPI, + app, auth, requireAuth, getContext, @@ -208,7 +211,7 @@ class User { } } -export default defineService({ +export const graphql = { Query: { me: User.me, info: () => "Public Data", @@ -216,43 +219,43 @@ export default defineService({ Mutation: { createUser: User.create, }, +}; + +// Initialize the authentication middleware +app.use("*", auth.initialize()); + +// Automatically try to create a user for each request for demonstration purposes +app.use(async (_, next) => { + try { + await User.create(); + } catch { + // Ignore errors + // Fail silently if the user already exists + } + + await next(); }); -export const configureApp: PylonAPI["configureApp"] = (app) => { - // Initialize the authentication middleware - app.use("*", auth.initialize()); +app.get("/api/info", (c) => { + return new Response("Public Data"); +}); - // Automatically try to create a user for each request for demonstration purposes - app.use(async (_, next) => { - try { - await User.create(); - } catch { - // Ignore errors - // Fail silently if the user already exists - } +// The `auth.require()` middleware is optional here, as the `User.me` method already checks for it. +app.get("/api/me", auth.require(), async (c) => { + const user = await User.me(); - await next(); - }); + return c.json(user); +}); - app.get("/api/info", (c) => { - return new Response("Public Data"); - }); +// A role check for `read:messages` is not required here, as the `user.messages` method already checks for it. +app.get("/api/me/messages", auth.require(), async (c) => { + const user = await User.me(); - // The `auth.require()` middleware is optional here, as the `User.me` method already checks for it. - app.get("/api/me", auth.require(), async (c) => { - const user = await User.me(); + // This will throw an error if the user does not have the `read:messages` role + return c.json(await user.messages()); +}); - return c.json(user); - }); - - // A role check for `read:messages` is not required here, as the `user.messages` method already checks for it. - app.get("/api/me/messages", auth.require(), async (c) => { - const user = await User.me(); - - // This will throw an error if the user does not have the `read:messages` role - return c.json(await user.messages()); - }); -}; +export default app; ``` ### Call the API @@ -273,7 +276,7 @@ export TOKEN='MtjHodGy4zxKylDOhg6kW90WeEQs2q...' Now you have to start the Pylon service: ```bash -bun run develop +bun run dev ``` With the access token, you can then do the following calls: diff --git a/docs/docs/guides/manage/customize/notification-providers.mdx b/docs/docs/guides/manage/customize/notification-providers.mdx new file mode 100644 index 0000000000..b94ce7542d --- /dev/null +++ b/docs/docs/guides/manage/customize/notification-providers.mdx @@ -0,0 +1,166 @@ +--- +title: SMS, SMTP and HTTP Provider for Notifications +sidebar_label: Notification Providers +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +ZITADEL can send messages to users via different notification providers, such as SMS, SMTP, or Webhook (HTTP Provider). +While you can add multiple providers to different channels, messages will only be delivered via the actived provider. +Message and notification texts can be [customized](./texts) for an instance or for each organization. + +## SMS providers + +ZITADEL integrates with Twilio as SMS provider. + +## SMTP providers + +Integration with most SMTP providers is possible through a generic SMTP provider template that allows you to configure custom SMTP providers. +Additionally, integration templates are available for: + +- Amazon SES +- Mailgun +- Mailjet +- Postmark +- Sendgrid + +:::info Default Provider ZITADEL Cloud +A default SMTP provider is configured for ZITADEL Cloud customers. +This provider meant for development and testing purposes and you must replace this provider with your custom SMTP provider for production use cases to guarantee security and reliability of your service. +::: + +## Webhook / HTTP provider + +Webhook (HTTP Provider) allows you to fully customize the messages and use integrate with any provider or custom solution to deliver the messages to users. +A provider with HTTP type will send the messages and the data to a pre-defined webhook as JSON. + +### Configuring a HTTP provider + + + + + First [add a new SMS Provider of type HTTP](/apis/resources/admin/admin-service-add-sms-provider-http) to create a new HTTP provider that can be used to send SMS messages: + + ```bash + curl -L 'https://$CUSTOM-DOMAIN/admin/v1/sms/http' \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' \ + -d '{ + "endpoint": "http://relay.example.com/provider", + "description": "provider description" + }' + ``` + + Where `endpoint` defines the Webhook endpoint to which the data should be sent to. + The result will contain an ID of the provider that we need in the next step. + + You can configure multiple SMS providers at the same time. + To use the HTTP provider you need to [activate the SMS provider](/apis/resources/admin/admin-service-activate-sms-provider): + + ```bash + curl -L 'https://$CUSTOM-DOMAIN/admin/v1/sms/:id/_activate' \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' \ + -d '{}' + ``` + + The `id` is the provider's ID from the previous step. + + See full API reference for [SMS Providers](/apis/resources/admin/sms-provider) for more details. + + + + + First [add a new Email Provider of type HTTP](/apis/resources/admin/admin-service-add-email-provider-http) to create a new HTTP provider that can be used to send SMS messages: + + ```bash + curl -L 'https://$CUSTOM-DOMAIN/admin/v1/email/http' \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' \ + -d '{ + "endpoint": "http://relay.example.com/provider", + "description": "provider description" + }' + ``` + + Where `endpoint` defines the Webhook endpoint to which the data should be sent to. + The result will contain an ID of the provider that we need in the next step. + + You can configure multiple Email providers at the same time. + To use the HTTP provider you need to [activate the Email provider](/apis/resources/admin/admin-service-activate-email-provider): + + ```bash + curl -L 'https://$CUSTOM-DOMAIN/admin/v1/email/:id/_activate' \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Authorization: Bearer ' \ + -d '{}' + ``` + + The `id` is the provider's ID from the previous step. + + See full API reference for [Email Providers](/apis/resources/admin/admin-service-list-email-providers) for more details. + + + + +### HTTP provider payload + +In case of the Twilio and Email providers, the messages will be sent as before, in case of the HTTP providers the content of the messages is the same but as a HTTP call. + +Here an example of the body of an payload sent via Email HTTP provider: + +```json +{ + "contextInfo": { + "eventType": "user.human.initialization.code.added", + "provider": { + "id": "285181292935381355", + "description": "test" + }, + "recipientEmailAddress": "example@zitadel.com" + }, + "templateData": { + "title": "Zitadel - Initialize User", + "preHeader": "Initialize User", + "subject": "Initialize User", + "greeting": "Hello GivenName FamilyName,", + "text": "This user was created in Zitadel. Use the username Username to login. Please click the button below to finish the initialization process. (Code 0M53RF) If you didn't ask for this mail, please ignore it.", + "url": "http://example.zitadel.com/ui/login/user/init?authRequestID=\u0026code=0M53RF\u0026loginname=Username\u0026orgID=275353657317327214\u0026passwordset=false\u0026userID=285181014567813483", + "buttonText": "Finish initialization", + "primaryColor": "#5469d4", + "backgroundColor": "#fafafa", + "fontColor": "#000000", + "fontFamily": "-apple-system, BlinkMacSystemFont, Segoe UI, Lato, Arial, Helvetica, sans-serif", + "footerText": "InitCode.Footer" + }, + "args": { + "changeDate": "2024-09-16T10:58:50.73237+02:00", + "code": "0M53RF", + "creationDate": "2024-09-16T10:58:50.73237+02:00", + "displayName": "GivenName FamilyName", + "firstName": "GivenName", + "lastEmail": "example@zitadel.com", + "lastName": "FamilyName", + "lastPhone": "+41791234567", + "loginNames": [ + "Username" + ], + "nickName": "", + "preferredLoginName": "Username", + "userName": "Username", + "verifiedEmail": "example@zitadel.com", + "verifiedPhone": "" + } +} +``` + +There are 3 elements to this message: + +- `contextInfo`, with information on why this message is sent like the Event, which Email or SMS provider is used and which recipient should receive this message +- `templateData`, with all texts and format information which can be used with a template to produce the desired message +- `args`, with the information provided to the user which can be used in the message to customize \ No newline at end of file diff --git a/docs/docs/guides/manage/customize/notifications.md b/docs/docs/guides/manage/customize/notifications.md deleted file mode 100644 index 6ab69a9335..0000000000 --- a/docs/docs/guides/manage/customize/notifications.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: SMS, SMTP and HTTP Provider for Notifications ---- - -All Notifications send as SMS and Email are customizable as that you can define your own providers, -which then send the notifications out. These providers can also be defined as an HTTP type, -and the text and content, which is used to send the SMS's and Emails will get send to a Webhook as JSON. - -With this everything can be customized or even custom logic can be implemented to use a not yet supported provider by ZITADEL. - -## How it works - -There is a default provider configured in ZITADEL Cloud, both for SMS's and Emails, but this default providers can be changed through the respective API's. - -This API's are provided on an instance level: -- [SMS Providers](/apis/resources/admin/sms-provider) -- [Email Providers](/apis/resources/admin/email-provider) - -To use a non-default provider just add, and then activate. There can only be 1 provider be activated at the same time. - -## Resulting messages - -In case of the Twilio and SMTP providers, the messages will be sent as before, in case of the HTTP providers the content of the messages is the same but as a HTTP call. - -Here an example of the body of an Email sent via HTTP provider: -```json -{ - "contextInfo": { - "eventType": "user.human.initialization.code.added", - "provider": { - "id": "285181292935381355", - "description": "test" - }, - "recipientEmailAddress": "example@zitadel.com" - }, - "templateData": { - "title": "Zitadel - Initialize User", - "preHeader": "Initialize User", - "subject": "Initialize User", - "greeting": "Hello GivenName FamilyName,", - "text": "This user was created in Zitadel. Use the username Username to login. Please click the button below to finish the initialization process. (Code 0M53RF) If you didn't ask for this mail, please ignore it.", - "url": "http://example.zitadel.com/ui/login/user/init?authRequestID=\u0026code=0M53RF\u0026loginname=Username\u0026orgID=275353657317327214\u0026passwordset=false\u0026userID=285181014567813483", - "buttonText": "Finish initialization", - "primaryColor": "#5469d4", - "backgroundColor": "#fafafa", - "fontColor": "#000000", - "fontFamily": "-apple-system, BlinkMacSystemFont, Segoe UI, Lato, Arial, Helvetica, sans-serif", - "footerText": "InitCode.Footer" - }, - "args": { - "changeDate": "2024-09-16T10:58:50.73237+02:00", - "code": "0M53RF", - "creationDate": "2024-09-16T10:58:50.73237+02:00", - "displayName": "GivenName FamilyName", - "firstName": "GivenName", - "lastEmail": "example@zitadel.com", - "lastName": "FamilyName", - "lastPhone": "+41791234567", - "loginNames": [ - "Username" - ], - "nickName": "", - "preferredLoginName": "Username", - "userName": "Username", - "verifiedEmail": "example@zitadel.com", - "verifiedPhone": "" - } -} -``` - -There are 3 elements to this message: -- contextInfo, with information on why this message is sent like the Event, which Email or SMS provider is used and which recipient should receive this message -- templateData, with all texts and format information which can be used with a template to produce the desired message -- args, with the information provided to the user which can be used in the message to customize diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 0318ad074a..c0c7d5a45c 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -187,7 +187,7 @@ module.exports = { selector: "div#", }, prism: { - additionalLanguages: ["csharp", "dart", "groovy", "regex", "java", "php", "python", "protobuf"], + additionalLanguages: ["csharp", "dart", "groovy", "regex", "java", "php", "python", "protobuf", "json", "bash"], }, colorMode: { defaultMode: "dark", diff --git a/docs/sidebars.js b/docs/sidebars.js index 4d8ec9f48d..1e26ec6074 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -154,11 +154,10 @@ module.exports = { type: "category", label: "Customize", items: [ - "guides/manage/customize/branding", - "guides/manage/customize/texts", - "guides/manage/customize/behavior", - "guides/manage/customize/restrictions", - "guides/manage/customize/notifications", + { + type: "autogenerated", + dirName: "guides/manage/customize", + }, ], }, { diff --git a/internal/api/grpc/auth/user_grant.go b/internal/api/grpc/auth/user_grant.go index 242a54b044..9a1842f9a9 100644 --- a/internal/api/grpc/auth/user_grant.go +++ b/internal/api/grpc/auth/user_grant.go @@ -55,5 +55,6 @@ func UserGrantToPb(grant *query.UserGrant) *auth_pb.UserGrant { ProjectGrantId: grant.GrantID, RoleKeys: grant.Roles, UserType: user.TypeToPb(grant.UserType), + State: user.UserGrantStateToPb(grant.State), } } diff --git a/internal/api/grpc/user/user_grant.go b/internal/api/grpc/user/user_grant.go index f4dd099409..eb7f2be205 100644 --- a/internal/api/grpc/user/user_grant.go +++ b/internal/api/grpc/user/user_grant.go @@ -23,7 +23,7 @@ func UserGrantToPb(assetPrefix string, grant *query.UserGrant) *user_pb.UserGran return &user_pb.UserGrant{ Id: grant.ID, UserId: grant.UserID, - State: user_pb.UserGrantState_USER_GRANT_STATE_ACTIVE, + State: UserGrantStateToPb(grant.State), RoleKeys: grant.Roles, ProjectId: grant.ProjectID, OrgId: grant.ResourceOwner, @@ -51,6 +51,21 @@ func UserGrantToPb(assetPrefix string, grant *query.UserGrant) *user_pb.UserGran } } +func UserGrantStateToPb(state domain.UserGrantState) user_pb.UserGrantState { + switch state { + case domain.UserGrantStateActive: + return user_pb.UserGrantState_USER_GRANT_STATE_ACTIVE + case domain.UserGrantStateInactive: + return user_pb.UserGrantState_USER_GRANT_STATE_INACTIVE + case domain.UserGrantStateRemoved, + domain.UserGrantStateUnspecified: + // these states should never occur here and are mainly listed for linting purposes + fallthrough + default: + return user_pb.UserGrantState_USER_GRANT_STATE_UNSPECIFIED + } +} + func UserGrantQueriesToQuery(ctx context.Context, queries []*user_pb.UserGrantQuery) (q []query.SearchQuery, err error) { q = make([]query.SearchQuery, len(queries)) for i, query := range queries { diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index 6bca54c671..08ed8c31b9 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -330,6 +330,9 @@ func (o *OPStorage) setUserinfo(ctx context.Context, userInfo *oidc.UserInfo, us if err != nil { return err } + if user.State != domain.UserStateActive { + return zerrors.ThrowUnauthenticated(nil, "OIDC-S3tha", "Errors.Users.NotActive") + } var allRoles bool roles := make([]string, 0) for _, scope := range scopes { @@ -799,19 +802,24 @@ func (o *OPStorage) assertRoles(ctx context.Context, userID, applicationID strin if projectID != "" { roleAudience = append(roleAudience, projectID) } - queries := make([]query.SearchQuery, 0, 2) projectQuery, err := query.NewUserGrantProjectIDsSearchQuery(roleAudience) if err != nil { return nil, nil, err } - queries = append(queries, projectQuery) userIDQuery, err := query.NewUserGrantUserIDSearchQuery(userID) if err != nil { return nil, nil, err } - queries = append(queries, userIDQuery) + activeQuery, err := query.NewUserGrantStateQuery(domain.UserGrantStateActive) + if err != nil { + return nil, nil, err + } grants, err := o.query.UserGrants(ctx, &query.UserGrantsQueries{ - Queries: queries, + Queries: []query.SearchQuery{ + projectQuery, + userIDQuery, + activeQuery, + }, }, true) if err != nil { return nil, nil, err diff --git a/internal/api/oidc/integration_test/token_client_credentials_test.go b/internal/api/oidc/integration_test/token_client_credentials_test.go index 5316202e31..372baf163d 100644 --- a/internal/api/oidc/integration_test/token_client_credentials_test.go +++ b/internal/api/oidc/integration_test/token_client_credentials_test.go @@ -24,6 +24,9 @@ func TestServer_ClientCredentialsExchange(t *testing.T) { machine, name, clientID, clientSecret, err := Instance.CreateOIDCCredentialsClient(CTX) require.NoError(t, err) + _, _, clientIDInactive, clientSecretInactive, err := Instance.CreateOIDCCredentialsClientInactive(CTX) + require.NoError(t, err) + type claims struct { name string username string @@ -71,6 +74,13 @@ func TestServer_ClientCredentialsExchange(t *testing.T) { scope: []string{oidc.ScopeOpenID}, wantErr: true, }, + { + name: "inactive machine user error", + clientID: clientIDInactive, + clientSecret: clientSecretInactive, + scope: []string{oidc.ScopeOpenID}, + wantErr: true, + }, { name: "wrong secret error", clientID: clientID, diff --git a/internal/api/oidc/userinfo.go b/internal/api/oidc/userinfo.go index 542ea6a083..b2121a73a2 100644 --- a/internal/api/oidc/userinfo.go +++ b/internal/api/oidc/userinfo.go @@ -66,7 +66,10 @@ func (s *Server) UserInfo(ctx context.Context, r *op.Request[oidc.UserInfoReques false, )(ctx, true, domain.TriggerTypePreUserinfoCreation) if err != nil { - return nil, err + if !zerrors.IsNotFound(err) { + return nil, err + } + return nil, op.NewStatusError(oidc.ErrAccessDenied().WithDescription("no active user").WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError), http.StatusUnauthorized) } return op.NewResponse(userInfo), nil } diff --git a/internal/api/saml/storage.go b/internal/api/saml/storage.go index 8791619ba0..76173c2592 100644 --- a/internal/api/saml/storage.go +++ b/internal/api/saml/storage.go @@ -131,6 +131,9 @@ func (p *Storage) SetUserinfoWithUserID(ctx context.Context, applicationID strin if err != nil { return err } + if user.State != domain.UserStateActive { + return zerrors.ThrowPreconditionFailed(nil, "SAML-S3gFd", "Errors.User.NotActive") + } userGrants, err := p.getGrants(ctx, userID, applicationID) if err != nil { @@ -157,6 +160,9 @@ func (p *Storage) SetUserinfoWithLoginName(ctx context.Context, userinfo models. if err != nil { return err } + if user.State != domain.UserStateActive { + return zerrors.ThrowPreconditionFailed(nil, "SAML-FJ262", "Errors.User.NotActive") + } setUserinfo(user, userinfo, attributes, map[string]*customAttribute{}) return nil @@ -324,10 +330,15 @@ func (p *Storage) getGrants(ctx context.Context, userID, applicationID string) ( if err != nil { return nil, err } + activeQuery, err := query.NewUserGrantStateQuery(domain.UserGrantStateActive) + if err != nil { + return nil, err + } return p.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{ projectQuery, userIDQuery, + activeQuery, }, }, true) } diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go index 9d7b574320..16bee6e7a4 100644 --- a/internal/auth/repository/eventsourcing/repository.go +++ b/internal/auth/repository/eventsourcing/repository.go @@ -11,6 +11,7 @@ import ( sd "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" eventstore2 "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/query" @@ -119,7 +120,11 @@ func (q queryViewWrapper) UserGrantsByProjectAndUserID(ctx context.Context, proj if err != nil { return nil, err } - queries := &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantUserID, userGrantProjectID}} + activeQuery, err := query.NewUserGrantStateQuery(domain.UserGrantStateActive) + if err != nil { + return nil, err + } + queries := &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantUserID, userGrantProjectID, activeQuery}} grants, err := q.Queries.UserGrants(ctx, queries, true) if err != nil { return nil, err diff --git a/internal/command/device_auth.go b/internal/command/device_auth.go index 1a151437b3..a2754650ea 100644 --- a/internal/command/device_auth.go +++ b/internal/command/device_auth.go @@ -144,7 +144,7 @@ func (c *Commands) CreateOIDCSessionFromDeviceAuth(ctx context.Context, deviceCo return nil, DeviceAuthStateError(deviceAuthModel.State) } - cmd, err := c.newOIDCSessionAddEvents(ctx, deviceAuthModel.UserOrgID) + cmd, err := c.newOIDCSessionAddEvents(ctx, deviceAuthModel.UserID, deviceAuthModel.UserOrgID) if err != nil { return nil, err } diff --git a/internal/command/device_auth_test.go b/internal/command/device_auth_test.go index 500c7b8f58..f25be7053a 100644 --- a/internal/command/device_auth_test.go +++ b/internal/command/device_auth_test.go @@ -126,7 +126,7 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) { pushErr := errors.New("pushErr") type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -149,7 +149,7 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) { { name: "not found error", fields: fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -169,7 +169,7 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) { { name: "push error", fields: fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(eventFromEventPusherWithInstanceID( "instance1", deviceauth.NewAddedEvent( @@ -211,7 +211,7 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) { { name: "success", fields: fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(eventFromEventPusherWithInstanceID( "instance1", deviceauth.NewAddedEvent( @@ -256,7 +256,7 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } gotDetails, err := c.ApproveDeviceAuth(tt.args.ctx, tt.args.id, tt.args.userID, tt.args.userOrgID, tt.args.authMethods, tt.args.authTime, tt.args.preferredLanguage, tt.args.userAgent, tt.args.sessionID) require.ErrorIs(t, err, tt.wantErr) @@ -271,7 +271,7 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { pushErr := errors.New("pushErr") type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -288,7 +288,7 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { { name: "not found error", fields: fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -298,7 +298,7 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { { name: "push error", fields: fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(eventFromEventPusherWithInstanceID( "instance1", deviceauth.NewAddedEvent( @@ -323,7 +323,7 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { { name: "success/denied", fields: fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(eventFromEventPusherWithInstanceID( "instance1", deviceauth.NewAddedEvent( @@ -350,7 +350,7 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { { name: "success/expired", fields: fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(eventFromEventPusherWithInstanceID( "instance1", deviceauth.NewAddedEvent( @@ -378,7 +378,7 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } gotDetails, err := c.CancelDeviceAuth(tt.args.ctx, tt.args.id, tt.args.reason) require.ErrorIs(t, err, tt.wantErr) @@ -586,6 +586,69 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { }, wantErr: DeviceAuthStateError(domain.DeviceAuthStateDone), }, + { + name: "user not active", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("123", "instance1"), + "clientID", "123", "456", time.Now().Add(-time.Minute), + []string{"openid", "offline_access"}, + []string{"audience"}, false, + ), + ), + eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewApprovedEvent(ctx, + deviceauth.NewAggregate("123", "instance1"), + "userID", "org1", + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + testNow, &language.Afrikaans, &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + "sessionID", + ), + ), + ), + expectFilter( + user.NewHumanAddedEvent( + ctx, + &user.NewAggregate("userID", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.English, + domain.GenderUnspecified, + "email", + false, + ), + user.NewUserDeactivatedEvent( + ctx, + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + ), + idGenerator: mock.NewIDGeneratorExpectIDs(t), + defaultAccessTokenLifetime: time.Hour, + defaultRefreshTokenLifetime: 7 * 24 * time.Hour, + defaultRefreshTokenIdleLifetime: 24 * time.Hour, + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx, + "123", + }, + wantErr: zerrors.ThrowPreconditionFailed(nil, "OIDCS-kj3g2", "Errors.User.NotActive"), + }, { name: "approved, success", fields: fields{ @@ -617,6 +680,21 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { ), ), ), + expectFilter( + user.NewHumanAddedEvent( + ctx, + &user.NewAggregate("userID", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.English, + domain.GenderUnspecified, + "email", + false, + ), + ), expectFilter(), // token lifetime expectPush( oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, @@ -699,6 +777,21 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { ), ), ), + expectFilter( + user.NewHumanAddedEvent( + ctx, + &user.NewAggregate("userID", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.English, + domain.GenderUnspecified, + "email", + false, + ), + ), expectFilter(), // token lifetime expectPush( oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, diff --git a/internal/command/oidc_session.go b/internal/command/oidc_session.go index ae201feb7e..1ad46ba7d6 100644 --- a/internal/command/oidc_session.go +++ b/internal/command/oidc_session.go @@ -80,7 +80,7 @@ func (c *Commands) CreateOIDCSessionFromAuthRequest(ctx context.Context, authReq return nil, "", err } - cmd, err := c.newOIDCSessionAddEvents(ctx, sessionModel.UserResourceOwner) + cmd, err := c.newOIDCSessionAddEvents(ctx, sessionModel.UserID, sessionModel.UserResourceOwner) if err != nil { return nil, "", err } @@ -141,7 +141,7 @@ func (c *Commands) CreateOIDCSession(ctx context.Context, ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - cmd, err := c.newOIDCSessionAddEvents(ctx, resourceOwner) + cmd, err := c.newOIDCSessionAddEvents(ctx, userID, resourceOwner) if err != nil { return nil, err } @@ -265,7 +265,14 @@ func (c *Commands) RevokeOIDCSessionToken(ctx context.Context, token, clientID s return c.pushAppendAndReduce(ctx, writeModel, oidcsession.NewAccessTokenRevokedEvent(ctx, writeModel.aggregate)) } -func (c *Commands) newOIDCSessionAddEvents(ctx context.Context, resourceOwner string, pending ...eventstore.Command) (*OIDCSessionEvents, error) { +func (c *Commands) newOIDCSessionAddEvents(ctx context.Context, userID, resourceOwner string, pending ...eventstore.Command) (*OIDCSessionEvents, error) { + userStateModel, err := c.userStateWriteModel(ctx, userID) + if err != nil { + return nil, err + } + if !userStateModel.UserState.IsEnabled() { + return nil, zerrors.ThrowPreconditionFailed(nil, "OIDCS-kj3g2", "Errors.User.NotActive") + } accessTokenLifetime, refreshTokenLifeTime, refreshTokenIdleLifetime, err := c.tokenTokenLifetimes(ctx) if err != nil { return nil, err @@ -281,6 +288,7 @@ func (c *Commands) newOIDCSessionAddEvents(ctx context.Context, resourceOwner st encryptionAlg: c.keyAlgorithm, events: pending, oidcSessionWriteModel: NewOIDCSessionWriteModel(sessionID, resourceOwner), + userStateModel: userStateModel, accessTokenLifetime: accessTokenLifetime, refreshTokenLifeTime: refreshTokenLifeTime, refreshTokenIdleLifetime: refreshTokenIdleLifetime, @@ -321,6 +329,13 @@ func (c *Commands) newOIDCSessionUpdateEvents(ctx context.Context, refreshToken if err = sessionWriteModel.CheckRefreshToken(refreshTokenID); err != nil { return nil, err } + userStateWriteModel, err := c.userStateWriteModel(ctx, sessionWriteModel.UserID) + if err != nil { + return nil, err + } + if !userStateWriteModel.UserState.IsEnabled() { + return nil, zerrors.ThrowPreconditionFailed(nil, "OIDCS-J39h2", "Errors.User.NotActive") + } accessTokenLifetime, refreshTokenLifeTime, refreshTokenIdleLifetime, err := c.tokenTokenLifetimes(ctx) if err != nil { return nil, err @@ -342,6 +357,7 @@ type OIDCSessionEvents struct { encryptionAlg crypto.EncryptionAlgorithm events []eventstore.Command oidcSessionWriteModel *OIDCSessionWriteModel + userStateModel *UserV2WriteModel accessTokenLifetime time.Duration refreshTokenLifeTime time.Duration diff --git a/internal/command/oidc_session_test.go b/internal/command/oidc_session_test.go index a3af86d25a..6d9ee6e32e 100644 --- a/internal/command/oidc_session_test.go +++ b/internal/command/oidc_session_test.go @@ -205,6 +205,103 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Flk38", "Errors.Session.NotExisting"), }, }, + { + "user not active", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + authrequest.NewAddedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate, + "loginClient", + "clientID", + "redirectURI", + "state", + "nonce", + []string{"openid", "offline_access"}, + []string{"audience"}, + domain.OIDCResponseTypeCode, + domain.OIDCResponseModeQuery, + &domain.OIDCCodeChallenge{ + Challenge: "challenge", + Method: domain.CodeChallengeMethodS256, + }, + []domain.Prompt{domain.PromptNone}, + []string{"en", "de"}, + gu.Ptr(time.Duration(0)), + gu.Ptr("loginHint"), + gu.Ptr("hintUserID"), + true, + ), + ), + eventFromEventPusher( + authrequest.NewCodeAddedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate), + ), + eventFromEventPusher( + authrequest.NewSessionLinkedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate, + "sessionID", + "userID", + testNow, + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + ), + ), + ), + expectFilter( + eventFromEventPusher( + session.NewAddedEvent(context.Background(), + &session.NewAggregate("sessionID", "instance1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + ), + ), + eventFromEventPusher( + session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, + "userID", "org1", testNow, &language.Afrikaans), + ), + eventFromEventPusher( + session.NewPasswordCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, + testNow), + ), + ), + expectFilter( + user.NewHumanAddedEvent( + context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + user.NewUserDeactivatedEvent( + context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + ), + idGenerator: mock.NewIDGeneratorExpectIDs(t), + defaultAccessTokenLifetime: time.Hour, + defaultRefreshTokenLifetime: 7 * 24 * time.Hour, + defaultRefreshTokenIdleLifetime: 24 * time.Hour, + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + authRequestID: "V2_authRequestID", + complianceCheck: mockAuthRequestComplianceChecker(nil), + needRefreshToken: true, + }, + res{ + err: zerrors.ThrowPreconditionFailed(nil, "OIDCS-kj3g2", "Errors.User.NotActive"), + }, + }, { "add successful", fields{ @@ -266,6 +363,21 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { testNow), ), ), + expectFilter( + user.NewHumanAddedEvent( + context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), expectFilter(), // token lifetime expectPush( authrequest.NewCodeExchangedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate), @@ -382,6 +494,21 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { testNow), ), ), + expectFilter( + user.NewHumanAddedEvent( + context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), expectFilter(), // token lifetime expectPush( oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, @@ -521,10 +648,81 @@ func TestCommands_CreateOIDCSession(t *testing.T) { }, wantErr: io.ErrClosedPipe, }, + { + name: "not active user", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + user.NewHumanAddedEvent( + context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + user.NewUserDeactivatedEvent( + context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + ), + idGenerator: mock.NewIDGeneratorExpectIDs(t), + defaultAccessTokenLifetime: time.Hour, + defaultRefreshTokenLifetime: 7 * 24 * time.Hour, + defaultRefreshTokenIdleLifetime: 24 * time.Hour, + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + userID: "userID", + resourceOwner: "org1", + clientID: "clientID", + audience: []string{"audience"}, + scope: []string{"openid", "offline_access"}, + authMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + authTime: testNow, + nonce: "nonce", + preferredLanguage: &language.Afrikaans, + userAgent: &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + reason: domain.TokenReasonAuthRequest, + actor: &domain.TokenActor{ + UserID: "user2", + Issuer: "foo.com", + }, + needRefreshToken: false, + }, + wantErr: zerrors.ThrowPreconditionFailed(nil, "OIDCS-kj3g2", "Errors.User.NotActive"), + }, { name: "without refresh token", fields: fields{ eventstore: expectEventstore( + expectFilter( + user.NewHumanAddedEvent( + context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), expectFilter(), // token lifetime expectPush( oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, @@ -606,6 +804,21 @@ func TestCommands_CreateOIDCSession(t *testing.T) { name: "with refresh token", fields: fields{ eventstore: expectEventstore( + expectFilter( + user.NewHumanAddedEvent( + context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), expectFilter(), // token lifetime expectPush( oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, @@ -689,6 +902,21 @@ func TestCommands_CreateOIDCSession(t *testing.T) { name: "with sessionID", fields: fields{ eventstore: expectEventstore( + expectFilter( + user.NewHumanAddedEvent( + context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), expectFilter(), // token lifetime expectPush( oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, @@ -772,6 +1000,21 @@ func TestCommands_CreateOIDCSession(t *testing.T) { name: "impersonation not allowed", fields: fields{ eventstore: expectEventstore( + expectFilter( + user.NewHumanAddedEvent( + context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), expectFilter(), // token lifetime ), idGenerator: mock.NewIDGeneratorExpectIDs(t, "oidcSessionID"), @@ -813,6 +1056,21 @@ func TestCommands_CreateOIDCSession(t *testing.T) { name: "impersonation allowed", fields: fields{ eventstore: expectEventstore( + expectFilter( + user.NewHumanAddedEvent( + context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), expectFilter(), // token lifetime expectPush( user.NewUserImpersonatedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, "clientID", &domain.TokenActor{ @@ -1067,6 +1325,63 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) { err: zerrors.ThrowPreconditionFailed(nil, "OIDCS-3jt2w", "Errors.OIDCSession.RefreshTokenInvalid"), }, }, + { + "user not active", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusherWithCreationDateNow( + oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, + "userID", "org1", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow, "nonce", &language.Afrikaans, + &domain.UserAgent{FingerprintID: gu.Ptr("browserFP")}, + ), + ), + eventFromEventPusherWithCreationDateNow( + oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, + "at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour, domain.TokenReasonAuthRequest, nil), + ), + eventFromEventPusherWithCreationDateNow( + oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, + "rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour), + ), + ), + expectFilter( + user.NewHumanAddedEvent( + context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + user.NewUserDeactivatedEvent( + context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + ), + idGenerator: mock.NewIDGeneratorExpectIDs(t), + defaultAccessTokenLifetime: time.Hour, + defaultRefreshTokenLifetime: 7 * 24 * time.Hour, + defaultRefreshTokenIdleLifetime: 24 * time.Hour, + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + refreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDp1c2VySUQ", //V2_oidcSessionID:rt_refreshTokenID:userID + scope: []string{"openid", "offline_access"}, + complianceCheck: mockRefreshTokenComplianceChecker(nil), + }, + res{ + err: zerrors.ThrowPreconditionFailed(nil, "OIDCS-J39h2", "Errors.User.NotActive"), + }, + }, { "refresh successful", fields{ @@ -1088,6 +1403,21 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) { "rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour), ), ), + expectFilter( + user.NewHumanAddedEvent( + context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), expectFilter(), // token lifetime expectPush( oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, @@ -1153,7 +1483,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) { func TestCommands_OIDCSessionByRefreshToken(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore idGenerator id.Generator defaultAccessTokenLifetime time.Duration defaultRefreshTokenLifetime time.Duration @@ -1177,7 +1507,7 @@ func TestCommands_OIDCSessionByRefreshToken(t *testing.T) { { "invalid refresh token format error", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args{ @@ -1191,7 +1521,7 @@ func TestCommands_OIDCSessionByRefreshToken(t *testing.T) { { "inactive session error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter(), ), keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), @@ -1207,7 +1537,7 @@ func TestCommands_OIDCSessionByRefreshToken(t *testing.T) { { "invalid refresh token error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, @@ -1235,7 +1565,7 @@ func TestCommands_OIDCSessionByRefreshToken(t *testing.T) { { "expired refresh token error", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, @@ -1267,7 +1597,7 @@ func TestCommands_OIDCSessionByRefreshToken(t *testing.T) { { "get successful", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusherWithCreationDateNow( oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, @@ -1316,7 +1646,7 @@ func TestCommands_OIDCSessionByRefreshToken(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, defaultAccessTokenLifetime: tt.fields.defaultAccessTokenLifetime, defaultRefreshTokenLifetime: tt.fields.defaultRefreshTokenLifetime, @@ -1348,7 +1678,7 @@ func TestCommands_OIDCSessionByRefreshToken(t *testing.T) { func TestCommands_RevokeOIDCSessionToken(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore keyAlgorithm crypto.EncryptionAlgorithm } type args struct { @@ -1368,7 +1698,7 @@ func TestCommands_RevokeOIDCSessionToken(t *testing.T) { { "invalid token", fields{ - eventstore: eventstoreExpect(t), + eventstore: expectEventstore(), keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args{ @@ -1382,7 +1712,7 @@ func TestCommands_RevokeOIDCSessionToken(t *testing.T) { { "refresh_token inactive", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, @@ -1407,7 +1737,7 @@ func TestCommands_RevokeOIDCSessionToken(t *testing.T) { { "refresh_token invalid client", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, @@ -1432,7 +1762,7 @@ func TestCommands_RevokeOIDCSessionToken(t *testing.T) { { "refresh_token revoked", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, @@ -1468,7 +1798,7 @@ func TestCommands_RevokeOIDCSessionToken(t *testing.T) { { "access_token inactive session", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, @@ -1493,7 +1823,7 @@ func TestCommands_RevokeOIDCSessionToken(t *testing.T) { { "access_token invalid client", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, @@ -1518,7 +1848,7 @@ func TestCommands_RevokeOIDCSessionToken(t *testing.T) { { "access_token revoked", fields{ - eventstore: eventstoreExpect(t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate, @@ -1555,7 +1885,7 @@ func TestCommands_RevokeOIDCSessionToken(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), keyAlgorithm: tt.fields.keyAlgorithm, } err := c.RevokeOIDCSessionToken(tt.args.ctx, tt.args.token, tt.args.clientID) diff --git a/internal/integration/oidc.go b/internal/integration/oidc.go index 3afd262a35..f6d779de95 100644 --- a/internal/integration/oidc.go +++ b/internal/integration/oidc.go @@ -22,6 +22,7 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/authn" "github.com/zitadel/zitadel/pkg/grpc/management" "github.com/zitadel/zitadel/pkg/grpc/user" + user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func (i *Instance) CreateOIDCClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, appType app.OIDCAppType, authMethod app.OIDCAuthMethodType, devMode bool, grantTypes ...app.OIDCGrantType) (*management.AddOIDCAppResponse, error) { @@ -355,6 +356,31 @@ func (i *Instance) CreateOIDCCredentialsClient(ctx context.Context) (machine *ma return machine, name, secret.GetClientId(), secret.GetClientSecret(), nil } +func (i *Instance) CreateOIDCCredentialsClientInactive(ctx context.Context) (machine *management.AddMachineUserResponse, name, clientID, clientSecret string, err error) { + name = gofakeit.Username() + machine, err = i.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{ + Name: name, + UserName: name, + AccessTokenType: user.AccessTokenType_ACCESS_TOKEN_TYPE_JWT, + }) + if err != nil { + return nil, "", "", "", err + } + secret, err := i.Client.Mgmt.GenerateMachineSecret(ctx, &management.GenerateMachineSecretRequest{ + UserId: machine.GetUserId(), + }) + if err != nil { + return nil, "", "", "", err + } + _, err = i.Client.UserV2.DeactivateUser(ctx, &user_v2.DeactivateUserRequest{ + UserId: machine.GetUserId(), + }) + if err != nil { + return nil, "", "", "", err + } + return machine, name, secret.GetClientId(), secret.GetClientSecret(), nil +} + func (i *Instance) CreateOIDCJWTProfileClient(ctx context.Context) (machine *management.AddMachineUserResponse, name string, keyData []byte, err error) { name = gofakeit.Username() machine, err = i.Client.Mgmt.AddMachineUser(ctx, &management.AddMachineUserRequest{ diff --git a/internal/query/user_grant.go b/internal/query/user_grant.go index c8e1dc05a7..e2bbabdc72 100644 --- a/internal/query/user_grant.go +++ b/internal/query/user_grant.go @@ -143,6 +143,10 @@ func NewUserGrantRoleQuery(value string) (SearchQuery, error) { return NewTextQuery(UserGrantRoles, value, TextListContains) } +func NewUserGrantStateQuery(value domain.UserGrantState) (SearchQuery, error) { + return NewNumberQuery(UserGrantState, value, NumberEquals) +} + func NewUserGrantWithGrantedQuery(owner string) (SearchQuery, error) { orgQuery, err := NewUserGrantResourceOwnerSearchQuery(owner) if err != nil { diff --git a/internal/query/userinfo_by_id.sql b/internal/query/userinfo_by_id.sql index 2c09215a69..cd7f301c72 100644 --- a/internal/query/userinfo_by_id.sql +++ b/internal/query/userinfo_by_id.sql @@ -2,7 +2,7 @@ with usr as ( select u.id, u.creation_date, u.change_date, u.sequence, u.state, u.resource_owner, u.username, n.login_name as preferred_login_name from projections.users13 u left join projections.login_names3 n on u.id = n.user_id and u.instance_id = n.instance_id - where u.id = $1 + where u.id = $1 and u.state = 1 -- only allow active users and u.instance_id = $2 and n.is_primary = true ), @@ -38,6 +38,7 @@ user_grants as ( where user_id = $1 and instance_id = $2 and project_id = any($3) + and state = 1 {{ if . -}} and resource_owner = any($4) {{- end }} diff --git a/proto/zitadel/auth.proto b/proto/zitadel/auth.proto index 0eae6d24e5..ed58d70569 100644 --- a/proto/zitadel/auth.proto +++ b/proto/zitadel/auth.proto @@ -1565,6 +1565,11 @@ message UserGrant { description: "type of the user (human / machine)" } ]; + zitadel.user.v1.UserGrantState state = 13 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the user grant"; + } + ]; } message ListMyProjectOrgsRequest {