@things-factory/auth-base 10.1.31 → 10.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/dist-server/controllers/invitation.d.ts +47 -10
  2. package/dist-server/controllers/invitation.js +159 -113
  3. package/dist-server/controllers/invitation.js.map +1 -1
  4. package/dist-server/controllers/profile.d.ts +1 -0
  5. package/dist-server/router/auth-public-process-router.js +36 -13
  6. package/dist-server/router/auth-public-process-router.js.map +1 -1
  7. package/dist-server/router/oauth2/oauth2-router.js +7 -2
  8. package/dist-server/router/oauth2/oauth2-router.js.map +1 -1
  9. package/dist-server/router/oauth2/oauth2-server.js +4 -4
  10. package/dist-server/router/oauth2/oauth2-server.js.map +1 -1
  11. package/dist-server/service/app-binding/app-binding-mutation.d.ts +27 -0
  12. package/dist-server/service/app-binding/app-binding-mutation.js +39 -6
  13. package/dist-server/service/app-binding/app-binding-mutation.js.map +1 -1
  14. package/dist-server/service/app-binding/app-binding-query.js +14 -7
  15. package/dist-server/service/app-binding/app-binding-query.js.map +1 -1
  16. package/dist-server/service/app-binding/app-binding.d.ts +9 -0
  17. package/dist-server/service/app-binding/app-binding.js +10 -1
  18. package/dist-server/service/app-binding/app-binding.js.map +1 -1
  19. package/dist-server/service/appliance/appliance-mutation.js +34 -1
  20. package/dist-server/service/appliance/appliance-mutation.js.map +1 -1
  21. package/dist-server/service/appliance/appliance-query.d.ts +2 -0
  22. package/dist-server/service/appliance/appliance-query.js +48 -0
  23. package/dist-server/service/appliance/appliance-query.js.map +1 -1
  24. package/dist-server/service/appliance/appliance.d.ts +1 -0
  25. package/dist-server/service/appliance/appliance.js +33 -3
  26. package/dist-server/service/appliance/appliance.js.map +1 -1
  27. package/dist-server/service/application/application-mutation.js +51 -6
  28. package/dist-server/service/application/application-mutation.js.map +1 -1
  29. package/dist-server/service/application/application.d.ts +9 -3
  30. package/dist-server/service/application/application.js +24 -8
  31. package/dist-server/service/application/application.js.map +1 -1
  32. package/dist-server/service/auth-provider/auth-provider-mutation.js +5 -0
  33. package/dist-server/service/auth-provider/auth-provider-mutation.js.map +1 -1
  34. package/dist-server/service/domain-generator/domain-generator-mutation.js +3 -0
  35. package/dist-server/service/domain-generator/domain-generator-mutation.js.map +1 -1
  36. package/dist-server/service/invitation/invitation-mutation.d.ts +17 -15
  37. package/dist-server/service/invitation/invitation-mutation.js +83 -56
  38. package/dist-server/service/invitation/invitation-mutation.js.map +1 -1
  39. package/dist-server/service/invitation/invitation-query.d.ts +20 -5
  40. package/dist-server/service/invitation/invitation-query.js +60 -19
  41. package/dist-server/service/invitation/invitation-query.js.map +1 -1
  42. package/dist-server/service/invitation/invitation.d.ts +14 -2
  43. package/dist-server/service/invitation/invitation.js +77 -8
  44. package/dist-server/service/invitation/invitation.js.map +1 -1
  45. package/dist-server/service/login-history/login-history-query.js +3 -0
  46. package/dist-server/service/login-history/login-history-query.js.map +1 -1
  47. package/dist-server/service/role/role-mutation.js +14 -15
  48. package/dist-server/service/role/role-mutation.js.map +1 -1
  49. package/dist-server/service/role/role-query.js +26 -4
  50. package/dist-server/service/role/role-query.js.map +1 -1
  51. package/dist-server/service/role/role-types.d.ts +2 -0
  52. package/dist-server/service/role/role-types.js +8 -0
  53. package/dist-server/service/role/role-types.js.map +1 -1
  54. package/dist-server/service/role-template/role-template-mutation.d.ts +17 -1
  55. package/dist-server/service/role-template/role-template-mutation.js +14 -3
  56. package/dist-server/service/role-template/role-template-mutation.js.map +1 -1
  57. package/dist-server/service/user/user-mutation.js +1 -0
  58. package/dist-server/service/user/user-mutation.js.map +1 -1
  59. package/dist-server/service/user/user.d.ts +1 -0
  60. package/dist-server/service/user/user.js +19 -4
  61. package/dist-server/service/user/user.js.map +1 -1
  62. package/dist-server/templates/invitation-email.d.ts +2 -1
  63. package/dist-server/templates/invitation-email.js +16 -4
  64. package/dist-server/templates/invitation-email.js.map +1 -1
  65. package/dist-server/tsconfig.tsbuildinfo +1 -1
  66. package/dist-server/utils/credential-at-rest.d.ts +28 -0
  67. package/dist-server/utils/credential-at-rest.js +36 -0
  68. package/dist-server/utils/credential-at-rest.js.map +1 -0
  69. package/dist-server/utils/credential-features.d.ts +35 -0
  70. package/dist-server/utils/credential-features.js +45 -0
  71. package/dist-server/utils/credential-features.js.map +1 -0
  72. package/dist-server/utils/credential-serial-rule.d.ts +41 -0
  73. package/dist-server/utils/credential-serial-rule.js +43 -0
  74. package/dist-server/utils/credential-serial-rule.js.map +1 -0
  75. package/dist-server/utils/invitation-state.d.ts +52 -0
  76. package/dist-server/utils/invitation-state.js +76 -0
  77. package/dist-server/utils/invitation-state.js.map +1 -0
  78. package/dist-server/utils/refuse-role-name.d.ts +42 -0
  79. package/dist-server/utils/refuse-role-name.js +76 -0
  80. package/dist-server/utils/refuse-role-name.js.map +1 -0
  81. package/dist-server/utils/role-name-standing.d.ts +58 -0
  82. package/dist-server/utils/role-name-standing.js +72 -0
  83. package/dist-server/utils/role-name-standing.js.map +1 -0
  84. package/package.json +4 -4
  85. package/tests/app-binding-delete-db.test.ts +188 -0
  86. package/tests/appliance-credential-state-db.test.ts +207 -0
  87. package/tests/appliance-delete-db.test.ts +119 -0
  88. package/tests/appliance-revoke-db.test.ts +73 -28
  89. package/tests/appliance-schema.test.ts +129 -0
  90. package/tests/application-delete-db.test.ts +190 -0
  91. package/tests/application-token-payload-db.test.ts +185 -0
  92. package/tests/checkin-privilege-seam-db.test.ts +158 -0
  93. package/tests/credential-at-rest-db.test.ts +246 -0
  94. package/tests/credential-features-off-db.test.ts +189 -0
  95. package/tests/credential-serial-rule.test.ts +84 -0
  96. package/tests/invitation-db.test.ts +416 -0
  97. package/tests/invitation-state.test.ts +92 -0
  98. package/tests/open-doors.test.ts +132 -0
  99. package/tests/role-inheritance-db.test.ts +286 -0
  100. package/tests/role-mutation-db.test.ts +21 -7
  101. package/tests/role-name-lookup-sentinel.test.ts +79 -0
  102. package/tests/role-name-standing.test.ts +83 -0
  103. package/tests/token-issuance.test.ts +7 -3
  104. package/translations/en.json +2 -0
  105. package/translations/ja.json +2 -0
  106. package/translations/ko.json +2 -0
  107. package/translations/ms.json +2 -0
  108. package/translations/zh.json +2 -0
@@ -1 +1 @@
1
- {"version":3,"file":"oauth2-router.js","sourceRoot":"","sources":["../../../server/router/oauth2/oauth2-router.ts"],"names":[],"mappings":"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","sourcesContent":["import jwt from 'jsonwebtoken'\nimport compose from 'koa-compose'\nimport passport from 'koa-passport'\nimport Router from 'koa-router'\n\nimport { Domain, getRepository } from '@things-factory/shell'\n\nimport { jwtAuthenticateMiddleware } from '../../middlewares/index.js'\nimport { Application } from '../../service/application/application.js'\nimport { User, UserStatus } from '../../service/user/user.js'\nimport { setAccessTokenCookie } from '../../utils/access-token-cookie.js'\nimport { SECRET } from '../../utils/get-secret.js'\nimport { server as oauth2orizeServer } from './oauth2-server.js'\nimport { Strategy as ClientPasswordStrategy } from './passport-oauth2-client-password.js'\n\ndeclare global {\n namespace NodeJS {\n interface Process {\n oauthDisconnect: (user: User) => Promise<void>\n }\n }\n}\n\nexport const oauth2Router = new Router()\n\npassport.use(\n 'oauth2-client-password',\n new ClientPasswordStrategy({}, (clientId, clientSecret, done) => {\n getRepository(Application)\n .findOneBy({\n appKey: clientId\n })\n .then(client => {\n if (!client || client.appSecret != clientSecret) {\n done(null, false)\n return\n }\n\n done(null, client)\n })\n .catch(err => done(err))\n })\n)\n\n// user decision endpoint\n//\n// `decision` middleware processes a user's decision to allow or deny access\n// requested by a client application. Based on the grant type requested by the\n// client, the above grant middleware configured above will be invoked to send\n// a response.\n\noauth2Router.post(\n '/decision',\n jwtAuthenticateMiddleware,\n compose(\n oauth2orizeServer.decision(async function (context) {\n const { request } = context\n\n return request.body\n })\n )\n)\n\n// token endpoint\n//\n// `token` middleware handles client requests to exchange authorization grants\n// for access tokens. Based on the grant type being exchanged, the above\n// exchange middleware will be invoked to handle the request. Clients must\n// authenticate when making requests to this endpoint.\n\noauth2Router.post(\n '/access-token',\n passport.authenticate('oauth2-client-password', { session: false }),\n oauth2orizeServer.token(),\n oauth2orizeServer.errorHandler()\n)\n\noauth2Router.post('/refresh-token', async (context, next) => {\n const refreshToken: string | undefined = context.request?.body?.refreshToken\n if (!refreshToken) throw new Error('Missing refresh token')\n\n const appUser: User | undefined = await getRepository(User).findOneBy({\n password: refreshToken\n })\n\n if (!appUser) throw new Error('App user is not found')\n\n try {\n jwt.verify(refreshToken, SECRET)\n const decoded = jwt.decode(refreshToken) as any\n const subdomain: string = decoded.domain.subdomain\n const domain: Domain | undefined = await getRepository(Domain).findOne({\n where: { subdomain }\n })\n if (!domain) throw new Error('Domain is not found')\n const appKey: string = decoded.application.appKey\n const scopes: any[] = decoded.scope\n\n const newAccessToken: string = Application.generateAccessToken(domain, appUser, appKey, scopes)\n const newRefreshToken: string = Application.generateRefreshToken(domain, appUser, appKey, scopes)\n\n appUser.password = newRefreshToken\n await getRepository(User).save(appUser)\n\n setAccessTokenCookie(context, newAccessToken)\n\n context.body = {\n accessToken: newAccessToken,\n refreshToken: newRefreshToken\n }\n } catch (e) {\n context.status = 401\n context.body = e.message\n }\n})\n\noauth2Router.get('/profile', jwtAuthenticateMiddleware, async (context, next) => {\n const { user, domain } = context.state\n\n const { name, description, email, userType: type, locale } = user\n const { name: domainName, subdomain, brandName, brandImage, contentImage, timezone } = domain || {}\n\n var application = {}\n if (type == 'application') {\n /* user entity에 reference 필드가 추가되기 전까지, appKey취득 방법임. */\n application['appKey'] = email.substr(0, email.lastIndexOf('@'))\n }\n\n context.body = {\n profile: {\n name,\n description,\n email,\n type /* (admin|user|application|appliance) */,\n domain: {\n name: domainName,\n subdomain,\n brandName,\n brandImage,\n contentImage,\n timezone\n },\n application\n }\n }\n})\n\noauth2Router.post('/disconnect', jwtAuthenticateMiddleware, async (context, next) => {\n try {\n let { user } = context.state\n\n if (typeof process.oauthDisconnect === 'function') {\n await process.oauthDisconnect(user)\n } else {\n user.domains = []\n user.roles = []\n user.status = UserStatus.DELETED\n await getRepository(User).save(user)\n }\n context.status = 200\n context.body = 'ok'\n } catch (e) {\n throw e\n }\n})\n"]}
1
+ {"version":3,"file":"oauth2-router.js","sourceRoot":"","sources":["../../../server/router/oauth2/oauth2-router.ts"],"names":[],"mappings":";;;;AAAA,wEAA8B;AAC9B,sEAAiC;AACjC,wEAAmC;AACnC,oEAA+B;AAE/B,iDAA6D;AAE7D,yDAAsE;AACtE,6EAAsE;AACtE,wDAA6D;AAC7D,+EAAyE;AACzE,6DAAkD;AAClD,yDAAgE;AAChE,6FAAyF;AAU5E,QAAA,YAAY,GAAG,IAAI,oBAAM,EAAE,CAAA;AAExC,sBAAQ,CAAC,GAAG,CACV,wBAAwB,EACxB,IAAI,6CAAsB,CAAC,EAAE,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,EAAE;IAC9D,IAAA,qBAAa,EAAC,4BAAW,CAAC;SACvB,SAAS,CAAC;QACT,MAAM,EAAE,QAAQ;KACjB,CAAC;SACD,IAAI,CAAC,MAAM,CAAC,EAAE;QACb,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,SAAS,IAAI,YAAY,EAAE,CAAC;YAChD,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;YACjB,OAAM;QACR,CAAC;QAED,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACpB,CAAC,CAAC;SACD,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;AAC5B,CAAC,CAAC,CACH,CAAA;AAED,yBAAyB;AACzB,EAAE;AACF,4EAA4E;AAC5E,+EAA+E;AAC/E,8EAA8E;AAC9E,cAAc;AAEd,oBAAY,CAAC,IAAI,CACf,WAAW,EACX,oCAAyB,EACzB,IAAA,qBAAO,EACL,yBAAiB,CAAC,QAAQ,CAAC,KAAK,WAAW,OAAO;IAChD,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IAE3B,OAAO,OAAO,CAAC,IAAI,CAAA;AACrB,CAAC,CAAC,CACH,CACF,CAAA;AAED,iBAAiB;AACjB,EAAE;AACF,8EAA8E;AAC9E,yEAAyE;AACzE,2EAA2E;AAC3E,sDAAsD;AAEtD,oBAAY,CAAC,IAAI,CACf,eAAe,EACf,sBAAQ,CAAC,YAAY,CAAC,wBAAwB,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EACnE,yBAAiB,CAAC,KAAK,EAAE,EACzB,yBAAiB,CAAC,YAAY,EAAE,CACjC,CAAA;AAED,oBAAY,CAAC,IAAI,CAAC,gBAAgB,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IAC1D,MAAM,YAAY,GAAuB,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,YAAY,CAAA;IAC5E,IAAI,CAAC,YAAY;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAA;IAE3D,MAAM,OAAO,GAAqB,MAAM,IAAA,qBAAa,EAAC,cAAI,CAAC,CAAC,SAAS,CAAC;QACpE,QAAQ,EAAE,YAAY;KACvB,CAAC,CAAA;IAEF,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAA;IAEtD,IAAI,CAAC;QACH,sBAAG,CAAC,MAAM,CAAC,YAAY,EAAE,sBAAM,CAAC,CAAA;QAChC,MAAM,OAAO,GAAG,sBAAG,CAAC,MAAM,CAAC,YAAY,CAAQ,CAAA;QAC/C,MAAM,SAAS,GAAW,OAAO,CAAC,MAAM,CAAC,SAAS,CAAA;QAClD,MAAM,MAAM,GAAuB,MAAM,IAAA,qBAAa,EAAC,cAAM,CAAC,CAAC,OAAO,CAAC;YACrE,KAAK,EAAE,EAAE,SAAS,EAAE;SACrB,CAAC,CAAA;QACF,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAA;QACnD,MAAM,MAAM,GAAW,OAAO,CAAC,WAAW,CAAC,MAAM,CAAA;QACjD,MAAM,MAAM,GAAU,OAAO,CAAC,KAAK,CAAA;QAEnC;;;;WAIG;QACH,MAAM,cAAc,GAAW,4BAAW,CAAC,mBAAmB,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,CAAA;QACnG,MAAM,eAAe,GAAW,4BAAW,CAAC,oBAAoB,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,CAAA;QAErG,OAAO,CAAC,QAAQ,GAAG,eAAe,CAAA;QAClC,MAAM,IAAA,qBAAa,EAAC,cAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAEvC,IAAA,6CAAoB,EAAC,OAAO,EAAE,cAAc,CAAC,CAAA;QAE7C,OAAO,CAAC,IAAI,GAAG;YACb,WAAW,EAAE,cAAc;YAC3B,YAAY,EAAE,eAAe;SAC9B,CAAA;IACH,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,MAAM,GAAG,GAAG,CAAA;QACpB,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC,OAAO,CAAA;IAC1B,CAAC;AACH,CAAC,CAAC,CAAA;AAEF,oBAAY,CAAC,GAAG,CAAC,UAAU,EAAE,oCAAyB,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IAC9E,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAEtC,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;IACjE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,GAAG,MAAM,IAAI,EAAE,CAAA;IAEnG,IAAI,WAAW,GAAG,EAAE,CAAA;IACpB,IAAI,IAAI,IAAI,aAAa,EAAE,CAAC;QAC1B,wDAAwD;QACxD,WAAW,CAAC,QAAQ,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAA;IACjE,CAAC;IAED,OAAO,CAAC,IAAI,GAAG;QACb,OAAO,EAAE;YACP,IAAI;YACJ,WAAW;YACX,KAAK;YACL,IAAI,CAAC,wCAAwC;YAC7C,MAAM,EAAE;gBACN,IAAI,EAAE,UAAU;gBAChB,SAAS;gBACT,SAAS;gBACT,UAAU;gBACV,YAAY;gBACZ,QAAQ;aACT;YACD,WAAW;SACZ;KACF,CAAA;AACH,CAAC,CAAC,CAAA;AAEF,oBAAY,CAAC,IAAI,CAAC,aAAa,EAAE,oCAAyB,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IAClF,IAAI,CAAC;QACH,IAAI,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAE5B,IAAI,OAAO,OAAO,CAAC,eAAe,KAAK,UAAU,EAAE,CAAC;YAClD,MAAM,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC,CAAA;QACrC,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,OAAO,GAAG,EAAE,CAAA;YACjB,IAAI,CAAC,KAAK,GAAG,EAAE,CAAA;YACf,IAAI,CAAC,MAAM,GAAG,oBAAU,CAAC,OAAO,CAAA;YAChC,MAAM,IAAA,qBAAa,EAAC,cAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACtC,CAAC;QACD,OAAO,CAAC,MAAM,GAAG,GAAG,CAAA;QACpB,OAAO,CAAC,IAAI,GAAG,IAAI,CAAA;IACrB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,CAAC,CAAA;IACT,CAAC;AACH,CAAC,CAAC,CAAA","sourcesContent":["import jwt from 'jsonwebtoken'\nimport compose from 'koa-compose'\nimport passport from 'koa-passport'\nimport Router from 'koa-router'\n\nimport { Domain, getRepository } from '@things-factory/shell'\n\nimport { jwtAuthenticateMiddleware } from '../../middlewares/index.js'\nimport { Application } from '../../service/application/application.js'\nimport { User, UserStatus } from '../../service/user/user.js'\nimport { setAccessTokenCookie } from '../../utils/access-token-cookie.js'\nimport { SECRET } from '../../utils/get-secret.js'\nimport { server as oauth2orizeServer } from './oauth2-server.js'\nimport { Strategy as ClientPasswordStrategy } from './passport-oauth2-client-password.js'\n\ndeclare global {\n namespace NodeJS {\n interface Process {\n oauthDisconnect: (user: User) => Promise<void>\n }\n }\n}\n\nexport const oauth2Router = new Router()\n\npassport.use(\n 'oauth2-client-password',\n new ClientPasswordStrategy({}, (clientId, clientSecret, done) => {\n getRepository(Application)\n .findOneBy({\n appKey: clientId\n })\n .then(client => {\n if (!client || client.appSecret != clientSecret) {\n done(null, false)\n return\n }\n\n done(null, client)\n })\n .catch(err => done(err))\n })\n)\n\n// user decision endpoint\n//\n// `decision` middleware processes a user's decision to allow or deny access\n// requested by a client application. Based on the grant type requested by the\n// client, the above grant middleware configured above will be invoked to send\n// a response.\n\noauth2Router.post(\n '/decision',\n jwtAuthenticateMiddleware,\n compose(\n oauth2orizeServer.decision(async function (context) {\n const { request } = context\n\n return request.body\n })\n )\n)\n\n// token endpoint\n//\n// `token` middleware handles client requests to exchange authorization grants\n// for access tokens. Based on the grant type being exchanged, the above\n// exchange middleware will be invoked to handle the request. Clients must\n// authenticate when making requests to this endpoint.\n\noauth2Router.post(\n '/access-token',\n passport.authenticate('oauth2-client-password', { session: false }),\n oauth2orizeServer.token(),\n oauth2orizeServer.errorHandler()\n)\n\noauth2Router.post('/refresh-token', async (context, next) => {\n const refreshToken: string | undefined = context.request?.body?.refreshToken\n if (!refreshToken) throw new Error('Missing refresh token')\n\n const appUser: User | undefined = await getRepository(User).findOneBy({\n password: refreshToken\n })\n\n if (!appUser) throw new Error('App user is not found')\n\n try {\n jwt.verify(refreshToken, SECRET)\n const decoded = jwt.decode(refreshToken) as any\n const subdomain: string = decoded.domain.subdomain\n const domain: Domain | undefined = await getRepository(Domain).findOne({\n where: { subdomain }\n })\n if (!domain) throw new Error('Domain is not found')\n const appKey: string = decoded.application.appKey\n const scopes: any[] = decoded.scope\n\n /*\n * This route reads the key back out of the token rather than holding the application, so it\n * names the field. The signer takes `{ appKey }` for exactly this reason — see the note on\n * `Application.sign`.\n */\n const newAccessToken: string = Application.generateAccessToken(domain, appUser, { appKey }, scopes)\n const newRefreshToken: string = Application.generateRefreshToken(domain, appUser, { appKey }, scopes)\n\n appUser.password = newRefreshToken\n await getRepository(User).save(appUser)\n\n setAccessTokenCookie(context, newAccessToken)\n\n context.body = {\n accessToken: newAccessToken,\n refreshToken: newRefreshToken\n }\n } catch (e) {\n context.status = 401\n context.body = e.message\n }\n})\n\noauth2Router.get('/profile', jwtAuthenticateMiddleware, async (context, next) => {\n const { user, domain } = context.state\n\n const { name, description, email, userType: type, locale } = user\n const { name: domainName, subdomain, brandName, brandImage, contentImage, timezone } = domain || {}\n\n var application = {}\n if (type == 'application') {\n /* user entity에 reference 필드가 추가되기 전까지, appKey취득 방법임. */\n application['appKey'] = email.substr(0, email.lastIndexOf('@'))\n }\n\n context.body = {\n profile: {\n name,\n description,\n email,\n type /* (admin|user|application|appliance) */,\n domain: {\n name: domainName,\n subdomain,\n brandName,\n brandImage,\n contentImage,\n timezone\n },\n application\n }\n }\n})\n\noauth2Router.post('/disconnect', jwtAuthenticateMiddleware, async (context, next) => {\n try {\n let { user } = context.state\n\n if (typeof process.oauthDisconnect === 'function') {\n await process.oauthDisconnect(user)\n } else {\n user.domains = []\n user.roles = []\n user.status = UserStatus.DELETED\n await getRepository(User).save(user)\n }\n context.status = 200\n context.body = 'ok'\n } catch (e) {\n throw e\n }\n})\n"]}
@@ -119,8 +119,8 @@ exports.server.exchange(oauth2orize_koa_1.default.exchange.code(async (client, c
119
119
  // await getRepository(User).save(appuser)
120
120
  // Lazy relation 필드들(domain, domains)들에 대한 업데이트. 이상의 방법으로 업데이트 해야하는 것 같다.
121
121
  // Lazy relation 업데이트 방법의 일관성이 부족하므로, Lazy relation 필드를 사용하지 않기를 권장함.
122
- var accessToken = application_js_1.Application.generateAccessToken(domain, appuser, appKey, scopes);
123
- var refreshToken = application_js_1.Application.generateRefreshToken(domain, appuser, appKey, scopes);
122
+ var accessToken = application_js_1.Application.generateAccessToken(domain, appuser, application, scopes);
123
+ var refreshToken = application_js_1.Application.generateRefreshToken(domain, appuser, application, scopes);
124
124
  await (0, shell_1.getRepository)(user_js_1.User).save({
125
125
  ...appuser,
126
126
  password: refreshToken
@@ -191,8 +191,8 @@ exports.server.exchange(oauth2orize_koa_1.default.exchange.refreshToken(async (c
191
191
  name: (0, typeorm_1.In)(scopes),
192
192
  domain: { id: domain.id }
193
193
  });
194
- var accessToken = application_js_1.Application.generateAccessToken(domain, appuser, appKey, scope);
195
- var refreshToken = application_js_1.Application.generateRefreshToken(domain, appuser, appKey, scope);
194
+ var accessToken = application_js_1.Application.generateAccessToken(domain, appuser, application, scope);
195
+ var refreshToken = application_js_1.Application.generateRefreshToken(domain, appuser, application, scope);
196
196
  await (0, shell_1.getRepository)(user_js_1.User).save({
197
197
  ...appuser,
198
198
  roles,
@@ -1 +1 @@
1
- {"version":3,"file":"oauth2-server.js","sourceRoot":"","sources":["../../../server/router/oauth2/oauth2-server.ts"],"names":[],"mappings":"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","sourcesContent":["import oauth2orize from 'oauth2orize-koa'\nimport { ILike, In } from 'typeorm'\n\nimport { logger } from '@things-factory/env'\nimport { Domain, getRepository } from '@things-factory/shell'\n\nimport { Application } from '../../service/application/application.js'\nimport { Role } from '../../service/role/role.js'\nimport { User, UserStatus } from '../../service/user/user.js'\n\nconst crypto = require('crypto')\n\nexport const NOTFOUND = 'NOTFOUND'\nexport const NonClient = {\n id: NOTFOUND\n}\n\n// create OAuth 2.0 server\nexport const server = oauth2orize.createServer()\n\n// Register serialialization and deserialization functions.\n//\n// When a client redirects a user to user authorization endpoint, an\n// authorization transaction is initiated. To complete the transaction, the\n// user must authenticate and approve the authorization request. Because this\n// may involve multiple HTTP request/response exchanges, the transaction is\n// stored in the session.\n//\n// An application must supply serialization functions, which determine how the\n// client object is serialized into the session. Typically this will be a\n// simple matter of serializing the client's ID, and deserializing by finding\n// the client by ID from the database.\n\nserver.serializeClient(async function (client) {\n return client.id\n})\n\nserver.deserializeClient(async function (id) {\n if (id == NOTFOUND) {\n return {}\n }\n\n const application = await getRepository(Application).findOneBy({ id })\n return application\n})\n\n// Register supported grant types.\n//\n// OAuth 2.0 specifies a framework that allows users to grant client\n// applications limited access to their protected resources. It does this\n// through a process of the user granting access, and the client exchanging\n// the grant for an access token.\n\n// Grant authorization codes. The callback takes the `client` requesting\n// authorization, the `redirectURI` (which is used as a verifier in the\n// subsequent exchange), the authenticated `user` granting access, and\n// their response, which contains approved scope, duration, etc. as parsed by\n// the application. The application issues a code, which is bound to these\n// values, and will be exchanged for an access token.\n\nserver.grant(\n oauth2orize.grant.code(async (client, redirectUrl, user, ares, areq) => {\n const { email, appKey, subdomain, scopes, state } = ares\n\n return Application.generateAuthCode(email, appKey, subdomain, scopes, state)\n })\n)\n\n// Exchange authorization codes for access tokens. The callback accepts the\n// `client`, which is exchanging `code` and any `redirectURI` from the\n// authorization request for verification. If these values are validated, the\n// application issues an access token on behalf of the user who authorized the\n// code.\n\nserver.exchange(\n oauth2orize.exchange.code(async (client, code, redirectUrl) => {\n try {\n /* authorization code */\n var decoded: any = Application.verifyAuthCode(code)\n } catch (e) {\n return false\n }\n let { email, appKey, subdomain, scopes } = decoded\n\n const application: Application = await getRepository(Application).findOneBy({\n appKey\n })\n\n if (!application) {\n return false\n }\n\n /* DONT-FORGET uncomment after test */\n // if (redirectUrl !== application.redirectUrl && redirectUrl.indexOf(application.redirectUrl) != 0) {\n // logger.error(\n // 'oauth2 exchange error - redirectUrl should begins with the application setting',\n // redirectUrl,\n // application.redirectUrl\n // )\n // // return false\n // throw new TypeError(\n // `oauth2 exchange error - redirectUrl should begins with the application setting : '${redirectUrl}':'${application.redirectUrl}'`\n // )\n // }\n\n const domain: Domain = await getRepository(Domain).findOneBy({\n subdomain\n })\n\n const creator: User = await getRepository(User).findOneBy({ email: ILike(email) })\n\n const appuserEmail = `${crypto.randomUUID()}@${subdomain}`\n\n var appuser: User = await getRepository(User).findOne({\n where: {\n email: appuserEmail,\n reference: application.id,\n userType: 'application'\n },\n\n relations: ['domains', 'creator', 'updater']\n })\n\n appuser = await getRepository(User).save({\n ...(appuser || {}),\n email: appuserEmail,\n name: application.name,\n userType: 'application',\n reference: application.id,\n domains: [domain],\n roles: scopes,\n status: UserStatus.ACTIVATED,\n updater: creator,\n creator\n })\n\n // appuser = await getRepository(User).findOne({\n // where: { email: ILike(appuserEmail) },\n // relations: ['domains']\n // })\n\n // appuser.domains = Promise.resolve([domain])\n // await getRepository(User).save(appuser)\n // Lazy relation 필드들(domain, domains)들에 대한 업데이트. 이상의 방법으로 업데이트 해야하는 것 같다.\n // Lazy relation 업데이트 방법의 일관성이 부족하므로, Lazy relation 필드를 사용하지 않기를 권장함.\n\n var accessToken = Application.generateAccessToken(domain, appuser, appKey, scopes)\n var refreshToken = Application.generateRefreshToken(domain, appuser, appKey, scopes)\n\n await getRepository(User).save({\n ...(appuser as any),\n password: refreshToken\n })\n\n return [\n accessToken,\n refreshToken,\n {\n expires_in: 30 * 24 * 60 * 60 /* 30d */,\n token_type: 'bearer',\n centerId: subdomain\n }\n ]\n })\n)\n\nserver.exchange(\n oauth2orize.exchange.refreshToken(async (client, refreshToken, scope) => {\n try {\n /* refresh token */\n var decoded: any = Application.verifyAuthCode(refreshToken)\n } catch (e) {\n logger.error(e)\n return false\n }\n const {\n id,\n userType,\n email,\n application: { appKey },\n domain: { subdomain },\n scope: originalScope,\n exp: expires_in\n } = decoded\n\n const application: Application = await getRepository(Application).findOneBy({\n appKey\n })\n\n if (!application) {\n logger.error('application is not exist')\n return false\n }\n\n if (Date.now() > expires_in * 1000) {\n logger.error('refresh token is expired')\n return false\n }\n\n const domain: Domain = await getRepository(Domain).findOneBy({\n subdomain\n })\n\n const creator: User = await getRepository(User).findOneBy({\n id,\n userType\n })\n\n const appuserEmail = `${appKey}@${subdomain}`\n\n var appuser: User = await getRepository(User).findOne({\n where: {\n email: appuserEmail,\n reference: application.id,\n userType: 'application'\n },\n relations: ['domain', 'creator', 'updater']\n })\n\n if (!appuser) {\n logger.error('application is not bound')\n return false\n }\n\n /*\n * `scope` is the scope of access requested by the client, which must not include any scope not originally granted.\n */\n\n scope = scope || originalScope\n\n const scopes: string[] = scope.split(',')\n const originalScopes = (originalScope || '').split(',')\n const additionalScope = scopes.find(scope => originalScopes.indexOf(scope) === -1)\n if (additionalScope) {\n logger.error(`additional scope(${additionalScope}) required`)\n return false\n }\n\n const roles = await getRepository(Role).findBy({\n name: In(scopes),\n domain: { id: domain.id }\n })\n\n var accessToken = Application.generateAccessToken(domain, appuser, appKey, scope)\n var refreshToken: any = Application.generateRefreshToken(domain, appuser, appKey, scope)\n\n await getRepository(User).save({\n ...(appuser as any),\n roles,\n password: refreshToken\n })\n\n return [\n accessToken,\n refreshToken,\n {\n expires_in: 30 * 24 * 60 * 60 /* 30d */,\n token_type: 'bearer'\n }\n ]\n })\n)\n"]}
1
+ {"version":3,"file":"oauth2-server.js","sourceRoot":"","sources":["../../../server/router/oauth2/oauth2-server.ts"],"names":[],"mappings":"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","sourcesContent":["import oauth2orize from 'oauth2orize-koa'\nimport { ILike, In } from 'typeorm'\n\nimport { logger } from '@things-factory/env'\nimport { Domain, getRepository } from '@things-factory/shell'\n\nimport { Application } from '../../service/application/application.js'\nimport { Role } from '../../service/role/role.js'\nimport { User, UserStatus } from '../../service/user/user.js'\n\nconst crypto = require('crypto')\n\nexport const NOTFOUND = 'NOTFOUND'\nexport const NonClient = {\n id: NOTFOUND\n}\n\n// create OAuth 2.0 server\nexport const server = oauth2orize.createServer()\n\n// Register serialialization and deserialization functions.\n//\n// When a client redirects a user to user authorization endpoint, an\n// authorization transaction is initiated. To complete the transaction, the\n// user must authenticate and approve the authorization request. Because this\n// may involve multiple HTTP request/response exchanges, the transaction is\n// stored in the session.\n//\n// An application must supply serialization functions, which determine how the\n// client object is serialized into the session. Typically this will be a\n// simple matter of serializing the client's ID, and deserializing by finding\n// the client by ID from the database.\n\nserver.serializeClient(async function (client) {\n return client.id\n})\n\nserver.deserializeClient(async function (id) {\n if (id == NOTFOUND) {\n return {}\n }\n\n const application = await getRepository(Application).findOneBy({ id })\n return application\n})\n\n// Register supported grant types.\n//\n// OAuth 2.0 specifies a framework that allows users to grant client\n// applications limited access to their protected resources. It does this\n// through a process of the user granting access, and the client exchanging\n// the grant for an access token.\n\n// Grant authorization codes. The callback takes the `client` requesting\n// authorization, the `redirectURI` (which is used as a verifier in the\n// subsequent exchange), the authenticated `user` granting access, and\n// their response, which contains approved scope, duration, etc. as parsed by\n// the application. The application issues a code, which is bound to these\n// values, and will be exchanged for an access token.\n\nserver.grant(\n oauth2orize.grant.code(async (client, redirectUrl, user, ares, areq) => {\n const { email, appKey, subdomain, scopes, state } = ares\n\n return Application.generateAuthCode(email, appKey, subdomain, scopes, state)\n })\n)\n\n// Exchange authorization codes for access tokens. The callback accepts the\n// `client`, which is exchanging `code` and any `redirectURI` from the\n// authorization request for verification. If these values are validated, the\n// application issues an access token on behalf of the user who authorized the\n// code.\n\nserver.exchange(\n oauth2orize.exchange.code(async (client, code, redirectUrl) => {\n try {\n /* authorization code */\n var decoded: any = Application.verifyAuthCode(code)\n } catch (e) {\n return false\n }\n let { email, appKey, subdomain, scopes } = decoded\n\n const application: Application = await getRepository(Application).findOneBy({\n appKey\n })\n\n if (!application) {\n return false\n }\n\n /* DONT-FORGET uncomment after test */\n // if (redirectUrl !== application.redirectUrl && redirectUrl.indexOf(application.redirectUrl) != 0) {\n // logger.error(\n // 'oauth2 exchange error - redirectUrl should begins with the application setting',\n // redirectUrl,\n // application.redirectUrl\n // )\n // // return false\n // throw new TypeError(\n // `oauth2 exchange error - redirectUrl should begins with the application setting : '${redirectUrl}':'${application.redirectUrl}'`\n // )\n // }\n\n const domain: Domain = await getRepository(Domain).findOneBy({\n subdomain\n })\n\n const creator: User = await getRepository(User).findOneBy({ email: ILike(email) })\n\n const appuserEmail = `${crypto.randomUUID()}@${subdomain}`\n\n var appuser: User = await getRepository(User).findOne({\n where: {\n email: appuserEmail,\n reference: application.id,\n userType: 'application'\n },\n\n relations: ['domains', 'creator', 'updater']\n })\n\n appuser = await getRepository(User).save({\n ...(appuser || {}),\n email: appuserEmail,\n name: application.name,\n userType: 'application',\n reference: application.id,\n domains: [domain],\n roles: scopes,\n status: UserStatus.ACTIVATED,\n updater: creator,\n creator\n })\n\n // appuser = await getRepository(User).findOne({\n // where: { email: ILike(appuserEmail) },\n // relations: ['domains']\n // })\n\n // appuser.domains = Promise.resolve([domain])\n // await getRepository(User).save(appuser)\n // Lazy relation 필드들(domain, domains)들에 대한 업데이트. 이상의 방법으로 업데이트 해야하는 것 같다.\n // Lazy relation 업데이트 방법의 일관성이 부족하므로, Lazy relation 필드를 사용하지 않기를 권장함.\n\n var accessToken = Application.generateAccessToken(domain, appuser, application, scopes)\n var refreshToken = Application.generateRefreshToken(domain, appuser, application, scopes)\n\n await getRepository(User).save({\n ...(appuser as any),\n password: refreshToken\n })\n\n return [\n accessToken,\n refreshToken,\n {\n expires_in: 30 * 24 * 60 * 60 /* 30d */,\n token_type: 'bearer',\n centerId: subdomain\n }\n ]\n })\n)\n\nserver.exchange(\n oauth2orize.exchange.refreshToken(async (client, refreshToken, scope) => {\n try {\n /* refresh token */\n var decoded: any = Application.verifyAuthCode(refreshToken)\n } catch (e) {\n logger.error(e)\n return false\n }\n const {\n id,\n userType,\n email,\n application: { appKey },\n domain: { subdomain },\n scope: originalScope,\n exp: expires_in\n } = decoded\n\n const application: Application = await getRepository(Application).findOneBy({\n appKey\n })\n\n if (!application) {\n logger.error('application is not exist')\n return false\n }\n\n if (Date.now() > expires_in * 1000) {\n logger.error('refresh token is expired')\n return false\n }\n\n const domain: Domain = await getRepository(Domain).findOneBy({\n subdomain\n })\n\n const creator: User = await getRepository(User).findOneBy({\n id,\n userType\n })\n\n const appuserEmail = `${appKey}@${subdomain}`\n\n var appuser: User = await getRepository(User).findOne({\n where: {\n email: appuserEmail,\n reference: application.id,\n userType: 'application'\n },\n relations: ['domain', 'creator', 'updater']\n })\n\n if (!appuser) {\n logger.error('application is not bound')\n return false\n }\n\n /*\n * `scope` is the scope of access requested by the client, which must not include any scope not originally granted.\n */\n\n scope = scope || originalScope\n\n const scopes: string[] = scope.split(',')\n const originalScopes = (originalScope || '').split(',')\n const additionalScope = scopes.find(scope => originalScopes.indexOf(scope) === -1)\n if (additionalScope) {\n logger.error(`additional scope(${additionalScope}) required`)\n return false\n }\n\n const roles = await getRepository(Role).findBy({\n name: In(scopes),\n domain: { id: domain.id }\n })\n\n var accessToken = Application.generateAccessToken(domain, appuser, application, scope)\n var refreshToken: any = Application.generateRefreshToken(domain, appuser, application, scope)\n\n await getRepository(User).save({\n ...(appuser as any),\n roles,\n password: refreshToken\n })\n\n return [\n accessToken,\n refreshToken,\n {\n expires_in: 30 * 24 * 60 * 60 /* 30d */,\n token_type: 'bearer'\n }\n ]\n })\n)\n"]}
@@ -1,3 +1,30 @@
1
1
  export declare class AppBindingMutation {
2
+ /**
3
+ * Cut one client off, without touching the application it was granted access to.
4
+ *
5
+ * ── This is the revocation ──────────────────────────────────────────────
6
+ * An appliance keeps its identity when its credential is taken away: past rows point at it as
7
+ * creator and updater, and a new secret should land on the same subject. A binding is not
8
+ * like that. The OAuth2 exchange makes one whenever a client is granted access, and the grant
9
+ * is the only thing it records — so deleting it is what revoking means here, and there is no
10
+ * second state for it to sit in (ADR-0055).
11
+ *
12
+ * ── What the criteria has to name ───────────────────────────────────────
13
+ * A binding is not a table of its own. It is a `User` row wearing a `userType`, which makes
14
+ * every account in the installation the same kind of row as this one. This ran
15
+ * `delete({ id })` — no type, no tenant — so any id at all deleted that row and answered
16
+ * true: a person's account, a domain owner, an appliance's shadow user, another tenant's
17
+ * binding. The domain was read out of the context and then not used.
18
+ *
19
+ * So the row is found first, the way `appBinding` finds one — this tenant, this type — and
20
+ * only then deleted. An id that is not one of ours answers exactly what a nonexistent id
21
+ * answers, for the same reason the read does: telling them apart would confirm that some id
22
+ * is real somewhere else.
23
+ *
24
+ * ── And it carried no privilege ─────────────────────────────────────────
25
+ * The schema recorded it as open, so anyone holding a valid token could reach it. It now asks
26
+ * for what `revokeApplianceCredential` asks for, which is the same act on the other kind of
27
+ * machine credential.
28
+ */
2
29
  deleteAppBinding(id: string, context: ResolverContext): Promise<boolean>;
3
30
  }
@@ -4,22 +4,55 @@ exports.AppBindingMutation = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const type_graphql_1 = require("type-graphql");
6
6
  const shell_1 = require("@things-factory/shell");
7
+ const get_domain_users_js_1 = require("../../utils/get-domain-users.js");
7
8
  const user_js_1 = require("../user/user.js");
8
9
  const app_binding_js_1 = require("./app-binding.js");
9
10
  let AppBindingMutation = class AppBindingMutation {
11
+ /**
12
+ * Cut one client off, without touching the application it was granted access to.
13
+ *
14
+ * ── This is the revocation ──────────────────────────────────────────────
15
+ * An appliance keeps its identity when its credential is taken away: past rows point at it as
16
+ * creator and updater, and a new secret should land on the same subject. A binding is not
17
+ * like that. The OAuth2 exchange makes one whenever a client is granted access, and the grant
18
+ * is the only thing it records — so deleting it is what revoking means here, and there is no
19
+ * second state for it to sit in (ADR-0055).
20
+ *
21
+ * ── What the criteria has to name ───────────────────────────────────────
22
+ * A binding is not a table of its own. It is a `User` row wearing a `userType`, which makes
23
+ * every account in the installation the same kind of row as this one. This ran
24
+ * `delete({ id })` — no type, no tenant — so any id at all deleted that row and answered
25
+ * true: a person's account, a domain owner, an appliance's shadow user, another tenant's
26
+ * binding. The domain was read out of the context and then not used.
27
+ *
28
+ * So the row is found first, the way `appBinding` finds one — this tenant, this type — and
29
+ * only then deleted. An id that is not one of ours answers exactly what a nonexistent id
30
+ * answers, for the same reason the read does: telling them apart would confirm that some id
31
+ * is real somewhere else.
32
+ *
33
+ * ── And it carried no privilege ─────────────────────────────────────────
34
+ * The schema recorded it as open, so anyone holding a valid token could reach it. It now asks
35
+ * for what `revokeApplianceCredential` asks for, which is the same act on the other kind of
36
+ * machine credential.
37
+ */
10
38
  async deleteAppBinding(id, context) {
11
39
  const { domain } = context.state;
12
- // TODO 이 사용자가 이 도메인에 속한 사용자인지 확인해야함.
13
- // TODO 다른 도메인에도 포함되어있다면, domains-users 관게와 해당 도메인 관련 정보만 삭제해야 함.
14
- await (0, shell_1.getRepository)(user_js_1.User).delete({
15
- id
16
- });
40
+ const binding = await (0, get_domain_users_js_1.buildDomainUsersQueryBuilder)(domain.id, 'USER')
41
+ .andWhere('USER.id = :id', { id })
42
+ .andWhere('USER.userType = :userType', { userType: app_binding_js_1.APP_BINDING_USER_TYPE })
43
+ .getOne();
44
+ if (!binding) {
45
+ return true;
46
+ }
47
+ // TODO 다른 도메인에도 포함되어있다면, domains-users 관계와 해당 도메인 관련 정보만 삭제해야 함.
48
+ await (0, shell_1.getRepository)(user_js_1.User).delete({ id: binding.id, userType: app_binding_js_1.APP_BINDING_USER_TYPE });
17
49
  return true;
18
50
  }
19
51
  };
20
52
  exports.AppBindingMutation = AppBindingMutation;
21
53
  tslib_1.__decorate([
22
- (0, type_graphql_1.Mutation)(returns => Boolean),
54
+ (0, type_graphql_1.Directive)('@privilege(category: "security", privilege: "mutation", domainOwnerGranted: true)'),
55
+ (0, type_graphql_1.Mutation)(returns => Boolean, { description: "To revoke an application's access by deleting its binding" }),
23
56
  tslib_1.__param(0, (0, type_graphql_1.Arg)('id')),
24
57
  tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
25
58
  tslib_1.__metadata("design:type", Function),
@@ -1 +1 @@
1
- {"version":3,"file":"app-binding-mutation.js","sourceRoot":"","sources":["../../../server/service/app-binding/app-binding-mutation.ts"],"names":[],"mappings":";;;;AAAA,+CAA2D;AAE3D,iDAAqD;AAErD,6CAAsC;AACtC,qDAA6C;AAGtC,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IAEvB,AAAN,KAAK,CAAC,gBAAgB,CAAY,EAAU,EAAS,OAAwB;QAC3E,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAEhC,qCAAqC;QACrC,iEAAiE;QACjE,MAAM,IAAA,qBAAa,EAAC,cAAI,CAAC,CAAC,MAAM,CAAC;YAC/B,EAAE;SACH,CAAC,CAAA;QAEF,OAAO,IAAI,CAAA;IACb,CAAC;CACF,CAAA;AAbY,gDAAkB;AAEvB;IADL,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC;IACL,mBAAA,IAAA,kBAAG,EAAC,IAAI,CAAC,CAAA;IAAc,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;0DAUnD;6BAZU,kBAAkB;IAD9B,IAAA,uBAAQ,EAAC,2BAAU,CAAC;GACR,kBAAkB,CAa9B","sourcesContent":["import { Arg, Ctx, Mutation, Resolver } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { User } from '../user/user.js'\nimport { AppBinding } from './app-binding.js'\n\n@Resolver(AppBinding)\nexport class AppBindingMutation {\n @Mutation(returns => Boolean)\n async deleteAppBinding(@Arg('id') id: string, @Ctx() context: ResolverContext) {\n const { domain } = context.state\n\n // TODO 이 사용자가 이 도메인에 속한 사용자인지 확인해야함.\n // TODO 다른 도메인에도 포함되어있다면, domains-users 관게와 해당 도메인 관련 정보만 삭제해야 함.\n await getRepository(User).delete({\n id\n })\n\n return true\n }\n}\n"]}
1
+ {"version":3,"file":"app-binding-mutation.js","sourceRoot":"","sources":["../../../server/service/app-binding/app-binding-mutation.ts"],"names":[],"mappings":";;;;AAAA,+CAAsE;AAEtE,iDAAqD;AAErD,yEAA8E;AAC9E,6CAAsC;AACtC,qDAAoE;AAG7D,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IAC7B;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IAGG,AAAN,KAAK,CAAC,gBAAgB,CAAY,EAAU,EAAS,OAAwB;QAC3E,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;QAEhC,MAAM,OAAO,GAAG,MAAM,IAAA,kDAA4B,EAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC;aAClE,QAAQ,CAAC,eAAe,EAAE,EAAE,EAAE,EAAE,CAAC;aACjC,QAAQ,CAAC,2BAA2B,EAAE,EAAE,QAAQ,EAAE,sCAAqB,EAAE,CAAC;aAC1E,MAAM,EAAE,CAAA;QAEX,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,IAAI,CAAA;QACb,CAAC;QAED,iEAAiE;QACjE,MAAM,IAAA,qBAAa,EAAC,cAAI,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,QAAQ,EAAE,sCAAqB,EAAE,CAAC,CAAA;QAErF,OAAO,IAAI,CAAA;IACb,CAAC;CACF,CAAA;AA/CY,gDAAkB;AA8BvB;IAFL,IAAA,wBAAS,EAAC,mFAAmF,CAAC;IAC9F,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,2DAA2D,EAAE,CAAC;IACnF,mBAAA,IAAA,kBAAG,EAAC,IAAI,CAAC,CAAA;IAAc,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;0DAgBnD;6BA9CU,kBAAkB;IAD9B,IAAA,uBAAQ,EAAC,2BAAU,CAAC;GACR,kBAAkB,CA+C9B","sourcesContent":["import { Arg, Ctx, Directive, Mutation, Resolver } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { buildDomainUsersQueryBuilder } from '../../utils/get-domain-users.js'\nimport { User } from '../user/user.js'\nimport { APP_BINDING_USER_TYPE, AppBinding } from './app-binding.js'\n\n@Resolver(AppBinding)\nexport class AppBindingMutation {\n /**\n * Cut one client off, without touching the application it was granted access to.\n *\n * ── This is the revocation ──────────────────────────────────────────────\n * An appliance keeps its identity when its credential is taken away: past rows point at it as\n * creator and updater, and a new secret should land on the same subject. A binding is not\n * like that. The OAuth2 exchange makes one whenever a client is granted access, and the grant\n * is the only thing it records — so deleting it is what revoking means here, and there is no\n * second state for it to sit in (ADR-0055).\n *\n * ── What the criteria has to name ───────────────────────────────────────\n * A binding is not a table of its own. It is a `User` row wearing a `userType`, which makes\n * every account in the installation the same kind of row as this one. This ran\n * `delete({ id })` — no type, no tenant — so any id at all deleted that row and answered\n * true: a person's account, a domain owner, an appliance's shadow user, another tenant's\n * binding. The domain was read out of the context and then not used.\n *\n * So the row is found first, the way `appBinding` finds one — this tenant, this type — and\n * only then deleted. An id that is not one of ours answers exactly what a nonexistent id\n * answers, for the same reason the read does: telling them apart would confirm that some id\n * is real somewhere else.\n *\n * ── And it carried no privilege ─────────────────────────────────────────\n * The schema recorded it as open, so anyone holding a valid token could reach it. It now asks\n * for what `revokeApplianceCredential` asks for, which is the same act on the other kind of\n * machine credential.\n */\n @Directive('@privilege(category: \"security\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Boolean, { description: \"To revoke an application's access by deleting its binding\" })\n async deleteAppBinding(@Arg('id') id: string, @Ctx() context: ResolverContext) {\n const { domain } = context.state\n\n const binding = await buildDomainUsersQueryBuilder(domain.id, 'USER')\n .andWhere('USER.id = :id', { id })\n .andWhere('USER.userType = :userType', { userType: APP_BINDING_USER_TYPE })\n .getOne()\n\n if (!binding) {\n return true\n }\n\n // TODO 다른 도메인에도 포함되어있다면, domains-users 관계와 해당 도메인 관련 정보만 삭제해야 함.\n await getRepository(User).delete({ id: binding.id, userType: APP_BINDING_USER_TYPE })\n\n return true\n }\n}\n"]}
@@ -9,12 +9,17 @@ const application_js_1 = require("../application/application.js");
9
9
  const user_js_1 = require("../user/user.js");
10
10
  const app_binding_js_1 = require("./app-binding.js");
11
11
  const app_binding_types_js_1 = require("./app-binding-types.js");
12
- /**
13
- * A binding is a `User` row wearing this type — the same value `oauth2-server.ts` writes when
14
- * an application is granted access. It is spelled once here so the list and the single lookup
15
- * cannot drift apart.
12
+ /*
13
+ * ── Why these two now carry a gate ──────────────────────────────────────────
14
+ * They carried none, so the schema recorded them as open and any valid token could list this
15
+ * tenant's bindings. The rows are tenant-scoped, and `AppBinding.refreshToken` has a gate of
16
+ * its own, so what was reachable was the rest of the row — the name, the email, and which
17
+ * application each client is bound to.
18
+ *
19
+ * The pair is the one the appliance and application lists ask for. Those three screens sit
20
+ * together and are the same kind of thing, and leaving one of them open reads as a decision
21
+ * that nobody made.
16
22
  */
17
- const APPLICATION_USER_TYPE = 'application';
18
23
  let AppBindingQuery = class AppBindingQuery {
19
24
  /**
20
25
  * One binding.
@@ -34,7 +39,7 @@ let AppBindingQuery = class AppBindingQuery {
34
39
  const { domain } = context.state;
35
40
  return await (0, get_domain_users_js_1.buildDomainUsersQueryBuilder)(domain.id, 'USER')
36
41
  .andWhere('USER.id = :id', { id })
37
- .andWhere('USER.userType = :userType', { userType: APPLICATION_USER_TYPE })
42
+ .andWhere('USER.userType = :userType', { userType: app_binding_js_1.APP_BINDING_USER_TYPE })
38
43
  .getOne();
39
44
  }
40
45
  /**
@@ -61,7 +66,7 @@ let AppBindingQuery = class AppBindingQuery {
61
66
  const { domain } = context.state;
62
67
  const alias = 'USER';
63
68
  const qb = (0, get_domain_users_js_1.buildDomainUsersQueryBuilder)(domain.id, alias);
64
- qb.andWhere(`${alias}.userType = :userType`, { userType: APPLICATION_USER_TYPE });
69
+ qb.andWhere(`${alias}.userType = :userType`, { userType: app_binding_js_1.APP_BINDING_USER_TYPE });
65
70
  (0, shell_1.buildQuery)(qb, params, null, { domainRef: false });
66
71
  const [items, total] = await qb.getManyAndCount();
67
72
  return { items, total };
@@ -85,6 +90,7 @@ let AppBindingQuery = class AppBindingQuery {
85
90
  };
86
91
  exports.AppBindingQuery = AppBindingQuery;
87
92
  tslib_1.__decorate([
93
+ (0, type_graphql_1.Directive)('@privilege(category: "user", privilege: "query", domainOwnerGranted: true, superUserGranted: true)'),
88
94
  (0, type_graphql_1.Query)(returns => app_binding_js_1.AppBinding),
89
95
  tslib_1.__param(0, (0, type_graphql_1.Arg)('id')),
90
96
  tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
@@ -93,6 +99,7 @@ tslib_1.__decorate([
93
99
  tslib_1.__metadata("design:returntype", Promise)
94
100
  ], AppBindingQuery.prototype, "appBinding", null);
95
101
  tslib_1.__decorate([
102
+ (0, type_graphql_1.Directive)('@privilege(category: "user", privilege: "query", domainOwnerGranted: true, superUserGranted: true)'),
96
103
  (0, type_graphql_1.Query)(returns => app_binding_types_js_1.AppBindingList),
97
104
  tslib_1.__param(0, (0, type_graphql_1.Args)(type => shell_1.ListParam)),
98
105
  tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
@@ -1 +1 @@
1
- {"version":3,"file":"app-binding-query.js","sourceRoot":"","sources":["../../../server/service/app-binding/app-binding-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Args, Ctx, FieldResolver, Query, Resolver, Root } from 'type-graphql'\nimport { SelectQueryBuilder } from 'typeorm'\n\nimport { buildQuery, getRepository, ListParam } from '@things-factory/shell'\n\nimport { buildDomainUsersQueryBuilder } from '../../utils/get-domain-users.js'\nimport { Application } from '../application/application.js'\nimport { User } from '../user/user.js'\nimport { UserList } from '../user/user-types.js'\nimport { AppBinding } from './app-binding.js'\nimport { AppBindingList } from './app-binding-types.js'\n\n/**\n * A binding is a `User` row wearing this type — the same value `oauth2-server.ts` writes when\n * an application is granted access. It is spelled once here so the list and the single lookup\n * cannot drift apart.\n */\nconst APPLICATION_USER_TYPE = 'application'\n\n@Resolver(AppBinding)\nexport class AppBindingQuery {\n /**\n * One binding.\n *\n * The domain was read out of the context and then not used — `findOneBy({ id, userType })`\n * searched every tenant, and a comment said so: \"TODO should check domain is available\".\n * An id from anywhere returned that application user's name, email and reference. The\n * refresh token was never part of it, since `AppBinding.refreshToken` carries its own\n * `@privilege(category: \"security\", …)`, but the rest of the row is still another tenant's.\n *\n * A binding belongs to the domain that made it, so the lookup asks for both. An id from\n * another domain now answers nothing, which is the same thing it answers for an id that\n * does not exist — deliberately: telling the two apart would confirm that some id is real\n * somewhere else.\n */\n @Query(returns => AppBinding)\n async appBinding(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<User> {\n const { domain } = context.state\n\n return await buildDomainUsersQueryBuilder(domain.id, 'USER')\n .andWhere('USER.id = :id', { id })\n .andWhere('USER.userType = :userType', { userType: APPLICATION_USER_TYPE })\n .getOne()\n }\n\n /**\n * The bindings of this domain.\n *\n * ── Paging used to happen before the filtering ───────────────────────────────\n * `buildQuery` applied the caller's pagination to the query, and the \"is this an\n * application user\" test ran in JavaScript on whatever came back:\n *\n * buildQuery(qb, params, …) take 20 users of any type\n * items = items.filter(user => user.userType == …) keep the ones that qualify\n * return { items, total: items.length } report that many in total\n *\n * A page of twenty therefore arrived with however many of those twenty happened to be\n * applications — three, or none — and the total reported the survivors of that one page.\n * The grid's pager believed it, so with more bindings than fit in a page the later ones\n * were unreachable: the pager never offered a page two that the total said did not exist.\n *\n * The condition belongs in the query, where the database can page what is already\n * filtered, and `getManyAndCount` then counts every matching row rather than the visible\n * ones.\n */\n @Query(returns => AppBindingList)\n async appBindings(@Args(type => ListParam) params: ListParam, @Ctx() context: ResolverContext): Promise<UserList> {\n const { domain } = context.state\n\n const alias: string = 'USER'\n const qb: SelectQueryBuilder<User> = buildDomainUsersQueryBuilder(domain.id, alias)\n\n qb.andWhere(`${alias}.userType = :userType`, { userType: APPLICATION_USER_TYPE })\n\n buildQuery(qb, params, null, { domainRef: false })\n\n const [items, total] = await qb.getManyAndCount()\n\n return { items, total }\n }\n\n @FieldResolver(type => Application)\n async application(@Root() appBinding: AppBinding): Promise<Application> {\n return await getRepository(Application).findOneBy({ id: appBinding.reference })\n }\n\n @FieldResolver(type => String)\n async scope(@Root() appBinding: AppBinding): Promise<string> {\n const u = await getRepository(User).findOne({ where: { reference: appBinding.reference }, relations: ['roles'] })\n return u.roles.map(role => role.name).join(',')\n }\n\n @FieldResolver(type => String)\n async refreshToken(@Root() appBinding: AppBinding): Promise<string> {\n return appBinding.password\n }\n\n @FieldResolver(type => User)\n async updater(@Root() appBinding: AppBinding): Promise<User> {\n return await getRepository(User).findOneBy({ id: appBinding.updaterId })\n }\n\n @FieldResolver(type => User)\n async creator(@Root() appBinding: AppBinding): Promise<User> {\n return await getRepository(User).findOneBy({ id: appBinding.creatorId })\n }\n}\n"]}
1
+ {"version":3,"file":"app-binding-query.js","sourceRoot":"","sources":["../../../server/service/app-binding/app-binding-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Args, Ctx, Directive, FieldResolver, Query, Resolver, Root } from 'type-graphql'\nimport { SelectQueryBuilder } from 'typeorm'\n\nimport { buildQuery, getRepository, ListParam } from '@things-factory/shell'\n\nimport { buildDomainUsersQueryBuilder } from '../../utils/get-domain-users.js'\nimport { Application } from '../application/application.js'\nimport { User } from '../user/user.js'\nimport { UserList } from '../user/user-types.js'\nimport { APP_BINDING_USER_TYPE, AppBinding } from './app-binding.js'\nimport { AppBindingList } from './app-binding-types.js'\n\n/*\n * ── Why these two now carry a gate ──────────────────────────────────────────\n * They carried none, so the schema recorded them as open and any valid token could list this\n * tenant's bindings. The rows are tenant-scoped, and `AppBinding.refreshToken` has a gate of\n * its own, so what was reachable was the rest of the row — the name, the email, and which\n * application each client is bound to.\n *\n * The pair is the one the appliance and application lists ask for. Those three screens sit\n * together and are the same kind of thing, and leaving one of them open reads as a decision\n * that nobody made.\n */\n@Resolver(AppBinding)\nexport class AppBindingQuery {\n /**\n * One binding.\n *\n * The domain was read out of the context and then not used — `findOneBy({ id, userType })`\n * searched every tenant, and a comment said so: \"TODO should check domain is available\".\n * An id from anywhere returned that application user's name, email and reference. The\n * refresh token was never part of it, since `AppBinding.refreshToken` carries its own\n * `@privilege(category: \"security\", …)`, but the rest of the row is still another tenant's.\n *\n * A binding belongs to the domain that made it, so the lookup asks for both. An id from\n * another domain now answers nothing, which is the same thing it answers for an id that\n * does not exist — deliberately: telling the two apart would confirm that some id is real\n * somewhere else.\n */\n @Directive('@privilege(category: \"user\", privilege: \"query\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => AppBinding)\n async appBinding(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<User> {\n const { domain } = context.state\n\n return await buildDomainUsersQueryBuilder(domain.id, 'USER')\n .andWhere('USER.id = :id', { id })\n .andWhere('USER.userType = :userType', { userType: APP_BINDING_USER_TYPE })\n .getOne()\n }\n\n /**\n * The bindings of this domain.\n *\n * ── Paging used to happen before the filtering ───────────────────────────────\n * `buildQuery` applied the caller's pagination to the query, and the \"is this an\n * application user\" test ran in JavaScript on whatever came back:\n *\n * buildQuery(qb, params, …) take 20 users of any type\n * items = items.filter(user => user.userType == …) keep the ones that qualify\n * return { items, total: items.length } report that many in total\n *\n * A page of twenty therefore arrived with however many of those twenty happened to be\n * applications — three, or none — and the total reported the survivors of that one page.\n * The grid's pager believed it, so with more bindings than fit in a page the later ones\n * were unreachable: the pager never offered a page two that the total said did not exist.\n *\n * The condition belongs in the query, where the database can page what is already\n * filtered, and `getManyAndCount` then counts every matching row rather than the visible\n * ones.\n */\n @Directive('@privilege(category: \"user\", privilege: \"query\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => AppBindingList)\n async appBindings(@Args(type => ListParam) params: ListParam, @Ctx() context: ResolverContext): Promise<UserList> {\n const { domain } = context.state\n\n const alias: string = 'USER'\n const qb: SelectQueryBuilder<User> = buildDomainUsersQueryBuilder(domain.id, alias)\n\n qb.andWhere(`${alias}.userType = :userType`, { userType: APP_BINDING_USER_TYPE })\n\n buildQuery(qb, params, null, { domainRef: false })\n\n const [items, total] = await qb.getManyAndCount()\n\n return { items, total }\n }\n\n @FieldResolver(type => Application)\n async application(@Root() appBinding: AppBinding): Promise<Application> {\n return await getRepository(Application).findOneBy({ id: appBinding.reference })\n }\n\n @FieldResolver(type => String)\n async scope(@Root() appBinding: AppBinding): Promise<string> {\n const u = await getRepository(User).findOne({ where: { reference: appBinding.reference }, relations: ['roles'] })\n return u.roles.map(role => role.name).join(',')\n }\n\n @FieldResolver(type => String)\n async refreshToken(@Root() appBinding: AppBinding): Promise<string> {\n return appBinding.password\n }\n\n @FieldResolver(type => User)\n async updater(@Root() appBinding: AppBinding): Promise<User> {\n return await getRepository(User).findOneBy({ id: appBinding.updaterId })\n }\n\n @FieldResolver(type => User)\n async creator(@Root() appBinding: AppBinding): Promise<User> {\n return await getRepository(User).findOneBy({ id: appBinding.creatorId })\n }\n}\n"]}
@@ -1,5 +1,14 @@
1
1
  import { Application } from '../application/application.js';
2
2
  import { User } from '../user/user.js';
3
+ /**
4
+ * What a binding is, as a row: a `User` with this `userType`.
5
+ *
6
+ * It is spelled here rather than in one of the resolvers because three places have to agree on
7
+ * it — the list, the single lookup, and the delete. The delete is the one that matters: a
8
+ * binding is not a distinct table, so a criteria that leaves this out names every account in
9
+ * the installation just as well.
10
+ */
11
+ export declare const APP_BINDING_USER_TYPE = "application";
3
12
  export declare class AppBinding extends User {
4
13
  application: Application;
5
14
  scope: string;
@@ -1,10 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AppBinding = void 0;
3
+ exports.AppBinding = exports.APP_BINDING_USER_TYPE = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const type_graphql_1 = require("type-graphql");
6
6
  const application_js_1 = require("../application/application.js");
7
7
  const user_js_1 = require("../user/user.js");
8
+ /**
9
+ * What a binding is, as a row: a `User` with this `userType`.
10
+ *
11
+ * It is spelled here rather than in one of the resolvers because three places have to agree on
12
+ * it — the list, the single lookup, and the delete. The delete is the one that matters: a
13
+ * binding is not a distinct table, so a criteria that leaves this out names every account in
14
+ * the installation just as well.
15
+ */
16
+ exports.APP_BINDING_USER_TYPE = 'application';
8
17
  let AppBinding = class AppBinding extends user_js_1.User {
9
18
  };
10
19
  exports.AppBinding = AppBinding;
@@ -1 +1 @@
1
- {"version":3,"file":"app-binding.js","sourceRoot":"","sources":["../../../server/service/app-binding/app-binding.ts"],"names":[],"mappings":";;;;AAAA,+CAA2D;AAC3D,kEAA2D;AAC3D,6CAAsC;AAK/B,IAAM,UAAU,GAAhB,MAAM,UAAW,SAAQ,cAAI;CAUnC,CAAA;AAVY,gCAAU;AAErB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,6CAA6C,EAAE,CAAC;sCACzE,4BAAW;+CAAA;AAGxB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,iDAAiD,EAAE,CAAC;;yCAC7E;AAIb;IAFC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,wCAAwC,EAAE,CAAC;IAChF,IAAA,wBAAS,EAAC,gFAAgF,CAAC;;gDACxE;qBATT,UAAU;IAHtB,IAAA,yBAAU,EAAC;QACV,WAAW,EAAE,wGAAwG;KACtH,CAAC;GACW,UAAU,CAUtB","sourcesContent":["import { ObjectType, Field, Directive } from 'type-graphql'\nimport { Application } from '../application/application.js'\nimport { User } from '../user/user.js'\n\n@ObjectType({\n description: 'Represents a user that is bound to a specific application, with an associated scope and refresh token.'\n})\nexport class AppBinding extends User {\n @Field({ nullable: true, description: 'The application to which the user is bound.' })\n application: Application\n\n @Field({ nullable: true, description: 'The scope of access granted to the application.' })\n scope: string\n\n @Field({ nullable: true, description: 'The refresh token for the application.' })\n @Directive('@privilege(category: \"security\", privilege: \"query\", domainOwnerGranted: true)')\n refreshToken: string\n}\n"]}
1
+ {"version":3,"file":"app-binding.js","sourceRoot":"","sources":["../../../server/service/app-binding/app-binding.ts"],"names":[],"mappings":";;;;AAAA,+CAA2D;AAC3D,kEAA2D;AAC3D,6CAAsC;AAEtC;;;;;;;GAOG;AACU,QAAA,qBAAqB,GAAG,aAAa,CAAA;AAK3C,IAAM,UAAU,GAAhB,MAAM,UAAW,SAAQ,cAAI;CAUnC,CAAA;AAVY,gCAAU;AAErB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,6CAA6C,EAAE,CAAC;sCACzE,4BAAW;+CAAA;AAGxB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,iDAAiD,EAAE,CAAC;;yCAC7E;AAIb;IAFC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,wCAAwC,EAAE,CAAC;IAChF,IAAA,wBAAS,EAAC,gFAAgF,CAAC;;gDACxE;qBATT,UAAU;IAHtB,IAAA,yBAAU,EAAC;QACV,WAAW,EAAE,wGAAwG;KACtH,CAAC;GACW,UAAU,CAUtB","sourcesContent":["import { ObjectType, Field, Directive } from 'type-graphql'\nimport { Application } from '../application/application.js'\nimport { User } from '../user/user.js'\n\n/**\n * What a binding is, as a row: a `User` with this `userType`.\n *\n * It is spelled here rather than in one of the resolvers because three places have to agree on\n * it — the list, the single lookup, and the delete. The delete is the one that matters: a\n * binding is not a distinct table, so a criteria that leaves this out names every account in\n * the installation just as well.\n */\nexport const APP_BINDING_USER_TYPE = 'application'\n\n@ObjectType({\n description: 'Represents a user that is bound to a specific application, with an associated scope and refresh token.'\n})\nexport class AppBinding extends User {\n @Field({ nullable: true, description: 'The application to which the user is bound.' })\n application: Application\n\n @Field({ nullable: true, description: 'The scope of access granted to the application.' })\n scope: string\n\n @Field({ nullable: true, description: 'The refresh token for the application.' })\n @Directive('@privilege(category: \"security\", privilege: \"query\", domainOwnerGranted: true)')\n refreshToken: string\n}\n"]}
@@ -5,6 +5,7 @@ const tslib_1 = require("tslib");
5
5
  const type_graphql_1 = require("type-graphql");
6
6
  const shell_1 = require("@things-factory/shell");
7
7
  const user_js_1 = require("../user/user.js");
8
+ const credential_features_js_1 = require("../../utils/credential-features.js");
8
9
  const appliance_js_1 = require("./appliance.js");
9
10
  const appliance_types_js_1 = require("./appliance-types.js");
10
11
  const crypto = require('crypto');
@@ -22,7 +23,20 @@ let ApplianceMutation = class ApplianceMutation {
22
23
  }
23
24
  async deleteAppliance(id, context) {
24
25
  const { domain, tx } = context.state;
25
- // TODO 이 사용자가 이 도메인에 속한 사용자인지 확인해야함.
26
+ /*
27
+ * The appliance is read before anything is deleted.
28
+ *
29
+ * The shadow user is found by `reference`, which carries no tenant of its own, and only the
30
+ * appliance row is scoped by domain. Deleting the user first meant an id belonging to
31
+ * somebody else took their device's identity away and then failed to delete their appliance:
32
+ * the row stayed on their screen looking registered while the device stopped at its next
33
+ * request, because `checkAuth` looks the subject up by the id in the token and there was no
34
+ * longer a subject. The mutation answered true either way, so nothing said it had happened.
35
+ */
36
+ const appliance = await (0, shell_1.getRepository)(appliance_js_1.Appliance, tx).findOneBy({ domain: { id: domain.id }, id });
37
+ if (!appliance) {
38
+ return true;
39
+ }
26
40
  // TODO 다른 도메인에도 포함되어있다면, domains-users 관게와 해당 도메인 관련 정보만 삭제해야 함.
27
41
  await (0, shell_1.getRepository)(user_js_1.User, tx).delete({
28
42
  reference: id,
@@ -38,6 +52,12 @@ let ApplianceMutation = class ApplianceMutation {
38
52
  if (!appuser.domains.find(d => d.id === domain.id)) {
39
53
  context.throw(401, 'appliance is not allowed for this domain');
40
54
  }
55
+ /*
56
+ * The serial moves before the token is signed, so the new token carries the new number and
57
+ * every earlier one stops matching (ADR-0085 decision 3). An appliance that has never been
58
+ * through this path starts at 1.
59
+ */
60
+ appuser.credentialSerial = (appuser.credentialSerial ?? 0) + 1;
41
61
  appuser.password = appliance_js_1.Appliance.generateAccessToken(domain, appuser, appliance);
42
62
  /*
43
63
  * Back to ACTIVATED. A revoked appliance is INACTIVE, and `checkAuth` refuses that whatever
@@ -90,6 +110,13 @@ let ApplianceMutation = class ApplianceMutation {
90
110
  */
91
111
  async revokeApplianceCredential(id, context) {
92
112
  const { domain, user, tx } = context.state;
113
+ /*
114
+ * The door only exists where the installation asked for it (ADR-0085 decision 2). Refusing
115
+ * here rather than hiding the mutation keeps one answer for a caller that reaches it anyway.
116
+ */
117
+ if (!(0, credential_features_js_1.offersCredentialRevocation)()) {
118
+ context.throw(404, 'credential revocation is not enabled on this installation');
119
+ }
93
120
  const applianceRepository = (0, shell_1.getRepository)(appliance_js_1.Appliance, tx);
94
121
  const appliance = await applianceRepository.findOneBy({ domain: { id: domain.id }, id });
95
122
  if (!appliance) {
@@ -99,6 +126,12 @@ let ApplianceMutation = class ApplianceMutation {
99
126
  if (appuser) {
100
127
  await (0, shell_1.getRepository)(user_js_1.User, tx).save({
101
128
  ...appuser,
129
+ /*
130
+ * The serial moves without a token being signed, so nothing matches it any more — not
131
+ * the token on the device, and not one issued before this appliance ever had a serial.
132
+ * The status alone could be undone by setting it back; this cannot.
133
+ */
134
+ credentialSerial: (appuser.credentialSerial ?? 0) + 1,
102
135
  status: user_js_1.UserStatus.INACTIVE,
103
136
  password: null,
104
137
  updater: user
@@ -1 +1 @@
1
- {"version":3,"file":"appliance-mutation.js","sourceRoot":"","sources":["../../../server/service/appliance/appliance-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Directive, Arg, Ctx, Mutation, Resolver } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { User, UserStatus } from '../user/user.js'\nimport { Appliance } from './appliance.js'\nimport { AppliancePatch, NewAppliance } from './appliance-types.js'\n\nconst crypto = require('crypto')\n\n@Resolver(Appliance)\nexport class ApplianceMutation {\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Appliance, { description: 'To create new appliance' })\n async createAppliance(\n @Arg('appliance') appliance: NewAppliance,\n @Ctx() context: ResolverContext\n ): Promise<Appliance> {\n const { domain, user, tx } = context.state\n\n const newAppliance = await getRepository(Appliance, tx).save({\n domain: domain,\n creator: user,\n updater: user,\n ...appliance\n })\n\n await Appliance.ensureApplianceUser(newAppliance, user, tx)\n\n return newAppliance\n }\n\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Boolean, { description: 'To delete appliance' })\n async deleteAppliance(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Boolean> {\n const { domain, tx } = context.state\n // TODO 이 사용자가 이 도메인에 속한 사용자인지 확인해야함.\n // TODO 다른 도메인에도 포함되어있다면, domains-users 관게와 해당 도메인 관련 정보만 삭제해야 함.\n await getRepository(User, tx).delete({\n reference: id,\n userType: 'appliance'\n })\n\n await getRepository(Appliance, tx).delete({ domain: { id: domain.id }, id })\n\n return true\n }\n\n @Directive('@privilege(category: \"security\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Appliance)\n async generateApplianceSecret(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Appliance> {\n const { domain, user, tx } = context.state\n\n const appliance: Appliance = await getRepository(Appliance, tx).findOneBy({ domain: { id: domain.id }, id })\n const appuser = await Appliance.ensureApplianceUser(appliance, user, tx)\n\n if (!appuser.domains.find(d => d.id === domain.id)) {\n context.throw(401, 'appliance is not allowed for this domain')\n }\n\n appuser.password = Appliance.generateAccessToken(domain, appuser, appliance)\n\n /*\n * Back to ACTIVATED. A revoked appliance is INACTIVE, and `checkAuth` refuses that whatever\n * the token says — so without this line a freshly issued secret would be dead on arrival.\n */\n appuser.status = UserStatus.ACTIVATED\n\n await getRepository(User).save(appuser)\n\n return await getRepository(Appliance, tx).save({\n ...appliance,\n accessToken: appuser.password,\n updater: user\n })\n }\n\n /*\n * Take an appliance's credential away without taking the appliance with it.\n *\n * ── Emptying the field is not revocation ────────────────────────────────\n * The access token is a **signed JWT**, not a secret we compare against. Clearing\n * `Appliance.accessToken` only drops our copy — the token already on the device keeps\n * verifying against SECRET until its own `exp`, which is a year out.\n *\n * What stops it is `User.checkAuth`. It runs on every request once the signature verifies,\n * looks the subject up by the id baked into the token, and refuses an INACTIVE one. So the\n * shadow user's status is the switch, and every outstanding token is refused from the next\n * request onward.\n *\n * ── Why INACTIVE and not LOCKED ─────────────────────────────────────────\n * LOCKED is the failed-sign-in lockout, and `checkAuth` lets it through once\n * `isAccountLocked` says the lock has aged out. A revocation that expires by itself would\n * hand a lost device its access back. INACTIVE is refused unconditionally.\n *\n * BANNED would read well here and does not work: it is in `UserStatus` but `checkAuth`'s\n * switch has no case for it, so a banned subject authenticates.\n *\n * Clearing `password` and `accessToken` is tidiness rather than a control: the JWT verifies\n * on its signature and never against those columns. ⚠ So a hazard remains — set the status\n * back to ACTIVATED without issuing a new secret and the old token authenticates again.\n * Closing that needs the token to be refusable on its own (a jti denylist), which this is\n * not.\n *\n * ── Why the shadow user survives ────────────────────────────────────────\n * `deleteAppliance` deletes it, and that is right when the appliance goes too. Here the\n * appliance stays, so the identity has to stay with it — it is what past rows point at as\n * creator and updater, and re-issuing later keeps the same id rather than orphaning them.\n *\n * ── What this is for ────────────────────────────────────────────────────\n * A device that is lost, decommissioned or suspect. Before this the only ways to stop one\n * were to delete the row, which takes the history with it, or to issue a new secret, which\n * leaves a working credential in the database and on the screen. Neither is revocation.\n */\n @Directive('@privilege(category: \"security\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Appliance, { description: \"To revoke an appliance's access credential\" })\n async revokeApplianceCredential(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Appliance> {\n const { domain, user, tx } = context.state\n\n const applianceRepository = getRepository(Appliance, tx)\n const appliance = await applianceRepository.findOneBy({ domain: { id: domain.id }, id })\n\n if (!appliance) {\n context.throw(404, 'appliance not found')\n }\n\n const appuser = await getRepository(User, tx).findOneBy({ reference: id, userType: 'appliance' })\n\n if (appuser) {\n await getRepository(User, tx).save({\n ...appuser,\n status: UserStatus.INACTIVE,\n password: null,\n updater: user\n })\n }\n\n return await applianceRepository.save({\n ...appliance,\n accessToken: null,\n updater: user\n })\n }\n\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Appliance)\n async updateAppliance(\n @Arg('id') id: string,\n @Arg('patch') patch: AppliancePatch,\n @Ctx() context: ResolverContext\n ): Promise<Appliance> {\n const { domain, user, tx } = context.state\n\n const applianceRepository = getRepository(Appliance, tx)\n const userRepository = getRepository(User, tx)\n\n const appliance = await applianceRepository.findOne({ where: { domain: { id: domain.id }, id } })\n const appuser = await Appliance.ensureApplianceUser(appliance, user, tx)\n\n await userRepository.save({\n ...appuser,\n name: patch?.name || user.name\n })\n\n return await applianceRepository.save({\n ...appliance,\n ...patch,\n updater: user\n })\n }\n}\n"]}
1
+ {"version":3,"file":"appliance-mutation.js","sourceRoot":"","sources":["../../../server/service/appliance/appliance-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Directive, Arg, Ctx, Mutation, Resolver } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { User, UserStatus } from '../user/user.js'\nimport { offersCredentialRevocation } from '../../utils/credential-features.js'\nimport { Appliance } from './appliance.js'\nimport { AppliancePatch, NewAppliance } from './appliance-types.js'\n\nconst crypto = require('crypto')\n\n@Resolver(Appliance)\nexport class ApplianceMutation {\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Appliance, { description: 'To create new appliance' })\n async createAppliance(\n @Arg('appliance') appliance: NewAppliance,\n @Ctx() context: ResolverContext\n ): Promise<Appliance> {\n const { domain, user, tx } = context.state\n\n const newAppliance = await getRepository(Appliance, tx).save({\n domain: domain,\n creator: user,\n updater: user,\n ...appliance\n })\n\n await Appliance.ensureApplianceUser(newAppliance, user, tx)\n\n return newAppliance\n }\n\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Boolean, { description: 'To delete appliance' })\n async deleteAppliance(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Boolean> {\n const { domain, tx } = context.state\n\n /*\n * The appliance is read before anything is deleted.\n *\n * The shadow user is found by `reference`, which carries no tenant of its own, and only the\n * appliance row is scoped by domain. Deleting the user first meant an id belonging to\n * somebody else took their device's identity away and then failed to delete their appliance:\n * the row stayed on their screen looking registered while the device stopped at its next\n * request, because `checkAuth` looks the subject up by the id in the token and there was no\n * longer a subject. The mutation answered true either way, so nothing said it had happened.\n */\n const appliance = await getRepository(Appliance, tx).findOneBy({ domain: { id: domain.id }, id })\n\n if (!appliance) {\n return true\n }\n\n // TODO 다른 도메인에도 포함되어있다면, domains-users 관게와 해당 도메인 관련 정보만 삭제해야 함.\n await getRepository(User, tx).delete({\n reference: id,\n userType: 'appliance'\n })\n\n await getRepository(Appliance, tx).delete({ domain: { id: domain.id }, id })\n\n return true\n }\n\n @Directive('@privilege(category: \"security\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Appliance)\n async generateApplianceSecret(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Appliance> {\n const { domain, user, tx } = context.state\n\n const appliance: Appliance = await getRepository(Appliance, tx).findOneBy({ domain: { id: domain.id }, id })\n const appuser = await Appliance.ensureApplianceUser(appliance, user, tx)\n\n if (!appuser.domains.find(d => d.id === domain.id)) {\n context.throw(401, 'appliance is not allowed for this domain')\n }\n\n /*\n * The serial moves before the token is signed, so the new token carries the new number and\n * every earlier one stops matching (ADR-0085 decision 3). An appliance that has never been\n * through this path starts at 1.\n */\n appuser.credentialSerial = (appuser.credentialSerial ?? 0) + 1\n appuser.password = Appliance.generateAccessToken(domain, appuser, appliance)\n\n /*\n * Back to ACTIVATED. A revoked appliance is INACTIVE, and `checkAuth` refuses that whatever\n * the token says — so without this line a freshly issued secret would be dead on arrival.\n */\n appuser.status = UserStatus.ACTIVATED\n\n await getRepository(User).save(appuser)\n\n return await getRepository(Appliance, tx).save({\n ...appliance,\n accessToken: appuser.password,\n updater: user\n })\n }\n\n /*\n * Take an appliance's credential away without taking the appliance with it.\n *\n * ── Emptying the field is not revocation ────────────────────────────────\n * The access token is a **signed JWT**, not a secret we compare against. Clearing\n * `Appliance.accessToken` only drops our copy — the token already on the device keeps\n * verifying against SECRET until its own `exp`, which is a year out.\n *\n * What stops it is `User.checkAuth`. It runs on every request once the signature verifies,\n * looks the subject up by the id baked into the token, and refuses an INACTIVE one. So the\n * shadow user's status is the switch, and every outstanding token is refused from the next\n * request onward.\n *\n * ── Why INACTIVE and not LOCKED ─────────────────────────────────────────\n * LOCKED is the failed-sign-in lockout, and `checkAuth` lets it through once\n * `isAccountLocked` says the lock has aged out. A revocation that expires by itself would\n * hand a lost device its access back. INACTIVE is refused unconditionally.\n *\n * BANNED would read well here and does not work: it is in `UserStatus` but `checkAuth`'s\n * switch has no case for it, so a banned subject authenticates.\n *\n * Clearing `password` and `accessToken` is tidiness rather than a control: the JWT verifies\n * on its signature and never against those columns. ⚠ So a hazard remains — set the status\n * back to ACTIVATED without issuing a new secret and the old token authenticates again.\n * Closing that needs the token to be refusable on its own (a jti denylist), which this is\n * not.\n *\n * ── Why the shadow user survives ────────────────────────────────────────\n * `deleteAppliance` deletes it, and that is right when the appliance goes too. Here the\n * appliance stays, so the identity has to stay with it — it is what past rows point at as\n * creator and updater, and re-issuing later keeps the same id rather than orphaning them.\n *\n * ── What this is for ────────────────────────────────────────────────────\n * A device that is lost, decommissioned or suspect. Before this the only ways to stop one\n * were to delete the row, which takes the history with it, or to issue a new secret, which\n * leaves a working credential in the database and on the screen. Neither is revocation.\n */\n @Directive('@privilege(category: \"security\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Appliance, { description: \"To revoke an appliance's access credential\" })\n async revokeApplianceCredential(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Appliance> {\n const { domain, user, tx } = context.state\n\n /*\n * The door only exists where the installation asked for it (ADR-0085 decision 2). Refusing\n * here rather than hiding the mutation keeps one answer for a caller that reaches it anyway.\n */\n if (!offersCredentialRevocation()) {\n context.throw(404, 'credential revocation is not enabled on this installation')\n }\n\n const applianceRepository = getRepository(Appliance, tx)\n const appliance = await applianceRepository.findOneBy({ domain: { id: domain.id }, id })\n\n if (!appliance) {\n context.throw(404, 'appliance not found')\n }\n\n const appuser = await getRepository(User, tx).findOneBy({ reference: id, userType: 'appliance' })\n\n if (appuser) {\n await getRepository(User, tx).save({\n ...appuser,\n /*\n * The serial moves without a token being signed, so nothing matches it any more — not\n * the token on the device, and not one issued before this appliance ever had a serial.\n * The status alone could be undone by setting it back; this cannot.\n */\n credentialSerial: (appuser.credentialSerial ?? 0) + 1,\n status: UserStatus.INACTIVE,\n password: null,\n updater: user\n })\n }\n\n return await applianceRepository.save({\n ...appliance,\n accessToken: null,\n updater: user\n })\n }\n\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Appliance)\n async updateAppliance(\n @Arg('id') id: string,\n @Arg('patch') patch: AppliancePatch,\n @Ctx() context: ResolverContext\n ): Promise<Appliance> {\n const { domain, user, tx } = context.state\n\n const applianceRepository = getRepository(Appliance, tx)\n const userRepository = getRepository(User, tx)\n\n const appliance = await applianceRepository.findOne({ where: { domain: { id: domain.id }, id } })\n const appuser = await Appliance.ensureApplianceUser(appliance, user, tx)\n\n await userRepository.save({\n ...appuser,\n name: patch?.name || user.name\n })\n\n return await applianceRepository.save({\n ...appliance,\n ...patch,\n updater: user\n })\n }\n}\n"]}
@@ -6,7 +6,9 @@ export declare class ApplianceQuery {
6
6
  appliance(id: string, context: ResolverContext): Promise<Appliance>;
7
7
  appliances(params: ListParam, context: ResolverContext): Promise<ApplianceList>;
8
8
  edges(params: ListParam, context: ResolverContext): Promise<ApplianceList>;
9
+ applianceCredentialRevocable(): Promise<boolean>;
9
10
  accessToken(appliance: Appliance, context: ResolverContext): Promise<string>;
11
+ credentialRevoked(appliance: Appliance): Promise<boolean>;
10
12
  domain(context: ResolverContext): Promise<Domain>;
11
13
  updater(appliance: Appliance): Promise<User>;
12
14
  creator(appliance: Appliance): Promise<User>;