-
Notifications
You must be signed in to change notification settings - Fork 105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: granular permission assignment for organization members #6231
Conversation
🐋 This PR was built and pushed to the following Docker images: Targets: Platforms: Image Tag: Docker Bake metadata{
"app": {
"buildx.build.provenance": {
"buildType": "https://mobyproject.org/buildkit@v1",
"materials": [
{
"uri": "pkg:docker/[email protected]?platform=linux%2Famd64",
"digest": {
"sha256": "f5a0871ab03b035c58bdb3007c3d177b001c2145c18e81817b71624dcf7d8bff"
}
}
],
"invocation": {
"configSource": {
"entryPoint": "services.dockerfile"
},
"parameters": {
"frontend": "dockerfile.v0",
"args": {
"build-arg:HEALTHCHECK_CMD": "wget --spider -q http://127.0.0.1:${PORT}/api/health",
"build-arg:IMAGE_DESCRIPTION": "The app of the GraphQL Hive project.",
"build-arg:IMAGE_TITLE": "graphql-hive/app",
"build-arg:PORT": "3000",
"build-arg:RELEASE": "d06ba598fd8ecc3a7d621a9877229a596f581d55",
"build-arg:SERVICE_DIR_NAME": "@hive/app",
"context:dist": "local:dist",
"context:shared": "local:shared",
"frontend.caps": "moby.buildkit.frontend.contexts+forward",
"local-sessionid:context": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:dockerfile": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:shared": "ojjxc7lh7rxj7w075fb9rgdfc"
},
"locals": [
{
"name": "context"
},
{
"name": "dist"
},
{
"name": "dockerfile"
},
{
"name": "shared"
}
]
},
"environment": {
"platform": "linux/amd64"
}
}
},
"buildx.build.ref": "builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c6/builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c60/t2e2hua3m4m9gsyu4bq6f2272",
"containerimage.config.digest": "sha256:e2ced83e62d4572e295f722db1c59fb9b488a980e40964ecdd008cabd695e74a",
"containerimage.descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:7c9abc53fe88ecfd1414d25baa2dc432b588efb85184138fab5d69d34b7c5bc2",
"size": 2075,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
"containerimage.digest": "sha256:7c9abc53fe88ecfd1414d25baa2dc432b588efb85184138fab5d69d34b7c5bc2",
"image.name": "ghcr.io/graphql-hive/app:d06ba598fd8ecc3a7d621a9877229a596f581d55-amd64,ghcr.io/graphql-hive/app:feat_permission_member_roles-amd64"
},
"buildx.build.warnings": [
{
"vertex": "sha256:8af84fb6a63a5ad58d473eaaed0553f175b34092dc843d96f59407ac19497b67",
"level": 1,
"short": "VW5kZWZpbmVkVmFyOiBVc2FnZSBvZiB1bmRlZmluZWQgdmFyaWFibGUgJyRJTUFHRV9ERVNDUklQVElPTicgKGxpbmUgMTQp",
"detail": [
"VmFyaWFibGVzIHNob3VsZCBiZSBkZWZpbmVkIGJlZm9yZSB0aGVpciB1c2U="
],
"url": "https://docs.docker.com/go/dockerfile/rule/undefined-var/",
"sourceInfo": {
"filename": "services.dockerfile",
"data": "RlJPTSBub2RlOjIyLjEzLjAtc2xpbQoKUlVOIGFwdC1nZXQgdXBkYXRlICYmIGFwdC1nZXQgaW5zdGFsbCAteSB3Z2V0IGNhLWNlcnRpZmljYXRlcyAmJiBybSAtcmYgL3Zhci9saWIvYXB0L2xpc3RzLyoKCkFSRyBTRVJWSUNFX0RJUl9OQU1FCldPUktESVIgL3Vzci9zcmMvYXBwLyRTRVJWSUNFX0RJUl9OQU1FCgpDT1BZIC0tZnJvbT1kaXN0IC4gL3Vzci9zcmMvYXBwLyRTRVJWSUNFX0RJUl9OQU1FLwpDT1BZIC0tZnJvbT1zaGFyZWQgLiAvCgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UubGljZW5zZXM9TUlUCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS50aXRsZT0kSU1BR0VfVElUTEUKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnZlcnNpb249JFJFTEVBU0UKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmRlc2NyaXB0aW9uPSRJTUFHRV9ERVNDUklQVElPTgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UuYXV0aG9ycz0iVGhlIEd1aWxkIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudmVuZG9yPSJLYW1pbCBLaXNpZWxhIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudXJsPSJodHRwczovL2dpdGh1Yi5jb20vZ3JhcGhxbC1oaXZlL3BsYXRmb3JtIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2Uuc291cmNlPSJodHRwczovL2dpdGh1Yi5jb20vZ3JhcGhxbC1oaXZlL3BsYXRmb3JtIgoKRU5WIEVOVklST05NRU5UIHByb2R1Y3Rpb24KRU5WIFJFTEVBU0UgJFJFTEVBU0UKRU5WIFBPUlQgJFBPUlQKCkhFQUxUSENIRUNLIC0taW50ZXJ2YWw9NXMgXAogIC0tdGltZW91dD01cyBcCiAgLS1zdGFydC1wZXJpb2Q9NXMgXAogIC0tcmV0cmllcz02IFwKICBDTUQgJEhFQUxUSENIRUNLX0NNRAoKRU5UUllQT0lOVCBbICIvZW50cnlwb2ludC5zaCIgXQo=",
"language": "Dockerfile"
},
"range": [
{
"start": {
"line": 14
},
"end": {
"line": 14
}
}
]
},
{
"vertex": "sha256:8af84fb6a63a5ad58d473eaaed0553f175b34092dc843d96f59407ac19497b67",
"level": 1,
"short": "VW5kZWZpbmVkVmFyOiBVc2FnZSBvZiB1bmRlZmluZWQgdmFyaWFibGUgJyRQT1JUJyAobGluZSAyMik=",
"detail": [
"VmFyaWFibGVzIHNob3VsZCBiZSBkZWZpbmVkIGJlZm9yZSB0aGVpciB1c2U="
],
"url": "https://docs.docker.com/go/dockerfile/rule/undefined-var/",
"sourceInfo": {
"filename": "services.dockerfile",
"data": "RlJPTSBub2RlOjIyLjEzLjAtc2xpbQoKUlVOIGFwdC1nZXQgdXBkYXRlICYmIGFwdC1nZXQgaW5zdGFsbCAteSB3Z2V0IGNhLWNlcnRpZmljYXRlcyAmJiBybSAtcmYgL3Zhci9saWIvYXB0L2xpc3RzLyoKCkFSRyBTRVJWSUNFX0RJUl9OQU1FCldPUktESVIgL3Vzci9zcmMvYXBwLyRTRVJWSUNFX0RJUl9OQU1FCgpDT1BZIC0tZnJvbT1kaXN0IC4gL3Vzci9zcmMvYXBwLyRTRVJWSUNFX0RJUl9OQU1FLwpDT1BZIC0tZnJvbT1zaGFyZWQgLiAvCgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UubGljZW5zZXM9TUlUCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS50aXRsZT0kSU1BR0VfVElUTEUKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnZlcnNpb249JFJFTEVBU0UKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmRlc2NyaXB0aW9uPSRJTUFHRV9ERVNDUklQVElPTgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UuYXV0aG9ycz0iVGhlIEd1aWxkIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudmVuZG9yPSJLYW1pbCBLaXNpZWxhIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudXJsPSJodHRwczovL2dpdGh1Yi5jb20vZ3JhcGhxbC1oaXZlL3BsYXRmb3JtIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2Uuc291cmNlPSJodHRwczovL2dpdGh1Yi5jb20vZ3JhcGhxbC1oaXZlL3BsYXRmb3JtIgoKRU5WIEVOVklST05NRU5UIHByb2R1Y3Rpb24KRU5WIFJFTEVBU0UgJFJFTEVBU0UKRU5WIFBPUlQgJFBPUlQKCkhFQUxUSENIRUNLIC0taW50ZXJ2YWw9NXMgXAogIC0tdGltZW91dD01cyBcCiAgLS1zdGFydC1wZXJpb2Q9NXMgXAogIC0tcmV0cmllcz02IFwKICBDTUQgJEhFQUxUSENIRUNLX0NNRAoKRU5UUllQT0lOVCBbICIvZW50cnlwb2ludC5zaCIgXQo=",
"language": "Dockerfile"
},
"range": [
{
"start": {
"line": 22
},
"end": {
"line": 22
}
}
]
},
{
"vertex": "sha256:8af84fb6a63a5ad58d473eaaed0553f175b34092dc843d96f59407ac19497b67",
"level": 1,
"short": "TGVnYWN5S2V5VmFsdWVGb3JtYXQ6ICJFTlYga2V5PXZhbHVlIiBzaG91bGQgYmUgdXNlZCBpbnN0ZWFkIG9mIGxlZ2FjeSAiRU5WIGtleSB2YWx1ZSIgZm9ybWF0IChsaW5lIDIyKQ==",
"detail": [
"TGVnYWN5IGtleS92YWx1ZSBmb3JtYXQgd2l0aCB3aGl0ZXNwYWNlIHNlcGFyYXRvciBzaG91bGQgbm90IGJlIHVzZWQ="
],
"url": "https://docs.docker.com/go/dockerfile/rule/legacy-key-value-format/",
"sourceInfo": {
"filename": "services.dockerfile",
"data": "RlJPTSBub2RlOjIyLjEzLjAtc2xpbQoKUlVOIGFwdC1nZXQgdXBkYXRlICYmIGFwdC1nZXQgaW5zdGFsbCAteSB3Z2V0IGNhLWNlcnRpZmljYXRlcyAmJiBybSAtcmYgL3Zhci9saWIvYXB0L2xpc3RzLyoKCkFSRyBTRVJWSUNFX0RJUl9OQU1FCldPUktESVIgL3Vzci9zcmMvYXBwLyRTRVJWSUNFX0RJUl9OQU1FCgpDT1BZIC0tZnJvbT1kaXN0IC4gL3Vzci9zcmMvYXBwLyRTRVJWSUNFX0RJUl9OQU1FLwpDT1BZIC0tZnJvbT1zaGFyZWQgLiAvCgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UubGljZW5zZXM9TUlUCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS50aXRsZT0kSU1BR0VfVElUTEUKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnZlcnNpb249JFJFTEVBU0UKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmRlc2NyaXB0aW9uPSRJTUFHRV9ERVNDUklQVElPTgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UuYXV0aG9ycz0iVGhlIEd1aWxkIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudmVuZG9yPSJLYW1pbCBLaXNpZWxhIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudXJsPSJodHRwczovL2dpdGh1Yi5jb20vZ3JhcGhxbC1oaXZlL3BsYXRmb3JtIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2Uuc291cmNlPSJodHRwczovL2dpdGh1Yi5jb20vZ3JhcGhxbC1oaXZlL3BsYXRmb3JtIgoKRU5WIEVOVklST05NRU5UIHByb2R1Y3Rpb24KRU5WIFJFTEVBU0UgJFJFTEVBU0UKRU5WIFBPUlQgJFBPUlQKCkhFQUxUSENIRUNLIC0taW50ZXJ2YWw9NXMgXAogIC0tdGltZW91dD01cyBcCiAgLS1zdGFydC1wZXJpb2Q9NXMgXAogIC0tcmV0cmllcz02IFwKICBDTUQgJEhFQUxUSENIRUNLX0NNRAoKRU5UUllQT0lOVCBbICIvZW50cnlwb2ludC5zaCIgXQo=",
"language": "Dockerfile"
},
"range": [
{
"start": {
"line": 22
},
"end": {
"line": 22
}
}
]
},
{
"vertex": "sha256:8af84fb6a63a5ad58d473eaaed0553f175b34092dc843d96f59407ac19497b67",
"level": 1,
"short": "VW5kZWZpbmVkVmFyOiBVc2FnZSBvZiB1bmRlZmluZWQgdmFyaWFibGUgJyRSRUxFQVNFJyAobGluZSAxMyk=",
"detail": [
"VmFyaWFibGVzIHNob3VsZCBiZSBkZWZpbmVkIGJlZm9yZSB0aGVpciB1c2U="
],
"url": "https://docs.docker.com/go/dockerfile/rule/undefined-var/",
"sourceInfo": {
"filename": "services.dockerfile",
"data": "RlJPTSBub2RlOjIyLjEzLjAtc2xpbQoKUlVOIGFwdC1nZXQgdXBkYXRlICYmIGFwdC1nZXQgaW5zdGFsbCAteSB3Z2V0IGNhLWNlcnRpZmljYXRlcyAmJiBybSAtcmYgL3Zhci9saWIvYXB0L2xpc3RzLyoKCkFSRyBTRVJWSUNFX0RJUl9OQU1FCldPUktESVIgL3Vzci9zcmMvYXBwLyRTRVJWSUNFX0RJUl9OQU1FCgpDT1BZIC0tZnJvbT1kaXN0IC4gL3Vzci9zcmMvYXBwLyRTRVJWSUNFX0RJUl9OQU1FLwpDT1BZIC0tZnJvbT1zaGFyZWQgLiAvCgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UubGljZW5zZXM9TUlUCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS50aXRsZT0kSU1BR0VfVElUTEUKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnZlcnNpb249JFJFTEVBU0UKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmRlc2NyaXB0aW9uPSRJTUFHRV9ERVNDUklQVElPTgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UuYXV0aG9ycz0iVGhlIEd1aWxkIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudmVuZG9yPSJLYW1pbCBLaXNpZWxhIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudXJsPSJodHRwczovL2dpdGh1Yi5jb20vZ3JhcGhxbC1oaXZlL3BsYXRmb3JtIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2Uuc291cmNlPSJodHRwczovL2dpdGh1Yi5jb20vZ3JhcGhxbC1oaXZlL3BsYXRmb3JtIgoKRU5WIEVOVklST05NRU5UIHByb2R1Y3Rpb24KRU5WIFJFTEVBU0UgJFJFTEVBU0UKRU5WIFBPUlQgJFBPUlQKCkhFQUxUSENIRUNLIC0taW50ZXJ2YWw9NXMgXAogIC0tdGltZW91dD01cyBcCiAgLS1zdGFydC1wZXJpb2Q9NXMgXAogIC0tcmV0cmllcz02IFwKICBDTUQgJEhFQUxUSENIRUNLX0NNRAoKRU5UUllQT0lOVCBbICIvZW50cnlwb2ludC5zaCIgXQo=",
"language": "Dockerfile"
},
"range": [
{
"start": {
"line": 13
},
"end": {
"line": 13
}
}
]
},
{
"vertex": "sha256:8af84fb6a63a5ad58d473eaaed0553f175b34092dc843d96f59407ac19497b67",
"level": 1,
"short": "VW5kZWZpbmVkVmFyOiBVc2FnZSBvZiB1bmRlZmluZWQgdmFyaWFibGUgJyRSRUxFQVNFJyAobGluZSAyMSk=",
"detail": [
"VmFyaWFibGVzIHNob3VsZCBiZSBkZWZpbmVkIGJlZm9yZSB0aGVpciB1c2U="
],
"url": "https://docs.docker.com/go/dockerfile/rule/undefined-var/",
"sourceInfo": {
"filename": "services.dockerfile",
"data": "RlJPTSBub2RlOjIyLjEzLjAtc2xpbQoKUlVOIGFwdC1nZXQgdXBkYXRlICYmIGFwdC1nZXQgaW5zdGFsbCAteSB3Z2V0IGNhLWNlcnRpZmljYXRlcyAmJiBybSAtcmYgL3Zhci9saWIvYXB0L2xpc3RzLyoKCkFSRyBTRVJWSUNFX0RJUl9OQU1FCldPUktESVIgL3Vzci9zcmMvYXBwLyRTRVJWSUNFX0RJUl9OQU1FCgpDT1BZIC0tZnJvbT1kaXN0IC4gL3Vzci9zcmMvYXBwLyRTRVJWSUNFX0RJUl9OQU1FLwpDT1BZIC0tZnJvbT1zaGFyZWQgLiAvCgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UubGljZW5zZXM9TUlUCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS50aXRsZT0kSU1BR0VfVElUTEUKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnZlcnNpb249JFJFTEVBU0UKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmRlc2NyaXB0aW9uPSRJTUFHRV9ERVNDUklQVElPTgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UuYXV0aG9ycz0iVGhlIEd1aWxkIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudmVuZG9yPSJLYW1pbCBLaXNpZWxhIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudXJsPSJodHRwczovL2dpdGh1Yi5jb20vZ3JhcGhxbC1oaXZlL3BsYXRmb3JtIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2Uuc291cmNlPSJodHRwczovL2dpdGh1Yi5jb20vZ3JhcGhxbC1oaXZlL3BsYXRmb3JtIgoKRU5WIEVOVklST05NRU5UIHByb2R1Y3Rpb24KRU5WIFJFTEVBU0UgJFJFTEVBU0UKRU5WIFBPUlQgJFBPUlQKCkhFQUxUSENIRUNLIC0taW50ZXJ2YWw9NXMgXAogIC0tdGltZW91dD01cyBcCiAgLS1zdGFydC1wZXJpb2Q9NXMgXAogIC0tcmV0cmllcz02IFwKICBDTUQgJEhFQUxUSENIRUNLX0NNRAoKRU5UUllQT0lOVCBbICIvZW50cnlwb2ludC5zaCIgXQo=",
"language": "Dockerfile"
},
"range": [
{
"start": {
"line": 21
},
"end": {
"line": 21
}
}
]
},
{
"vertex": "sha256:8af84fb6a63a5ad58d473eaaed0553f175b34092dc843d96f59407ac19497b67",
"level": 1,
"short": "TGVnYWN5S2V5VmFsdWVGb3JtYXQ6ICJFTlYga2V5PXZhbHVlIiBzaG91bGQgYmUgdXNlZCBpbnN0ZWFkIG9mIGxlZ2FjeSAiRU5WIGtleSB2YWx1ZSIgZm9ybWF0IChsaW5lIDIxKQ==",
"detail": [
"TGVnYWN5IGtleS92YWx1ZSBmb3JtYXQgd2l0aCB3aGl0ZXNwYWNlIHNlcGFyYXRvciBzaG91bGQgbm90IGJlIHVzZWQ="
],
"url": "https://docs.docker.com/go/dockerfile/rule/legacy-key-value-format/",
"sourceInfo": {
"filename": "services.dockerfile",
"data": "RlJPTSBub2RlOjIyLjEzLjAtc2xpbQoKUlVOIGFwdC1nZXQgdXBkYXRlICYmIGFwdC1nZXQgaW5zdGFsbCAteSB3Z2V0IGNhLWNlcnRpZmljYXRlcyAmJiBybSAtcmYgL3Zhci9saWIvYXB0L2xpc3RzLyoKCkFSRyBTRVJWSUNFX0RJUl9OQU1FCldPUktESVIgL3Vzci9zcmMvYXBwLyRTRVJWSUNFX0RJUl9OQU1FCgpDT1BZIC0tZnJvbT1kaXN0IC4gL3Vzci9zcmMvYXBwLyRTRVJWSUNFX0RJUl9OQU1FLwpDT1BZIC0tZnJvbT1zaGFyZWQgLiAvCgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UubGljZW5zZXM9TUlUCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS50aXRsZT0kSU1BR0VfVElUTEUKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnZlcnNpb249JFJFTEVBU0UKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmRlc2NyaXB0aW9uPSRJTUFHRV9ERVNDUklQVElPTgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UuYXV0aG9ycz0iVGhlIEd1aWxkIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudmVuZG9yPSJLYW1pbCBLaXNpZWxhIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudXJsPSJodHRwczovL2dpdGh1Yi5jb20vZ3JhcGhxbC1oaXZlL3BsYXRmb3JtIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2Uuc291cmNlPSJodHRwczovL2dpdGh1Yi5jb20vZ3JhcGhxbC1oaXZlL3BsYXRmb3JtIgoKRU5WIEVOVklST05NRU5UIHByb2R1Y3Rpb24KRU5WIFJFTEVBU0UgJFJFTEVBU0UKRU5WIFBPUlQgJFBPUlQKCkhFQUxUSENIRUNLIC0taW50ZXJ2YWw9NXMgXAogIC0tdGltZW91dD01cyBcCiAgLS1zdGFydC1wZXJpb2Q9NXMgXAogIC0tcmV0cmllcz02IFwKICBDTUQgJEhFQUxUSENIRUNLX0NNRAoKRU5UUllQT0lOVCBbICIvZW50cnlwb2ludC5zaCIgXQo=",
"language": "Dockerfile"
},
"range": [
{
"start": {
"line": 21
},
"end": {
"line": 21
}
}
]
},
{
"vertex": "sha256:4b57f193ffa97fd94332140f2aaed15c3281844a18761dd2750125c0906b7440",
"level": 1,
"short": "TGVnYWN5S2V5VmFsdWVGb3JtYXQ6ICJFTlYga2V5PXZhbHVlIiBzaG91bGQgYmUgdXNlZCBpbnN0ZWFkIG9mIGxlZ2FjeSAiRU5WIGtleSB2YWx1ZSIgZm9ybWF0IChsaW5lIDExKQ==",
"detail": [
"TGVnYWN5IGtleS92YWx1ZSBmb3JtYXQgd2l0aCB3aGl0ZXNwYWNlIHNlcGFyYXRvciBzaG91bGQgbm90IGJlIHVzZWQ="
],
"url": "https://docs.docker.com/go/dockerfile/rule/legacy-key-value-format/",
"sourceInfo": {
"filename": "migrations.dockerfile",
"data": "RlJPTSBub2RlOjIyLjEzLjAtc2xpbQoKUlVOIGFwdC1nZXQgdXBkYXRlICYmIGFwdC1nZXQgaW5zdGFsbCAteSBjYS1jZXJ0aWZpY2F0ZXMKCldPUktESVIgL3Vzci9zcmMvYXBwCgpDT1BZIC0tZnJvbT1kaXN0IC4gL3Vzci9zcmMvYXBwLwpDT1BZIC0tZnJvbT1zaGFyZWQgLiAvCgpFTlYgRU5WSVJPTk1FTlQgcHJvZHVjdGlvbgpFTlYgTk9ERV9FTlYgcHJvZHVjdGlvbgpFTlYgUkVMRUFTRSAkUkVMRUFTRQoKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmxpY2Vuc2VzPU1JVApMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudGl0bGU9JElNQUdFX1RJVExFCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS52ZXJzaW9uPSRSRUxFQVNFCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS5kZXNjcmlwdGlvbj0kSU1BR0VfREVTQ1JJUFRJT04KTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmF1dGhvcnM9IlRoZSBHdWlsZCIKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnZlbmRvcj0iS2FtaWwgS2lzaWVsYSIKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnVybD0iaHR0cHM6Ly9naXRodWIuY29tL2dyYXBocWwtaGl2ZS9wbGF0Zm9ybSIKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnNvdXJjZT0iaHR0cHM6Ly9naXRodWIuY29tL2dyYXBocWwtaGl2ZS9wbGF0Zm9ybSIKCkVOVFJZUE9JTlQgWyAiL2VudHJ5cG9pbnQuc2giIF0K",
"language": "Dockerfile"
},
"range": [
{
"start": {
"line": 11
},
"end": {
"line": 11
}
}
]
},
{
"vertex": "sha256:8af84fb6a63a5ad58d473eaaed0553f175b34092dc843d96f59407ac19497b67",
"level": 1,
"short": "TGVnYWN5S2V5VmFsdWVGb3JtYXQ6ICJFTlYga2V5PXZhbHVlIiBzaG91bGQgYmUgdXNlZCBpbnN0ZWFkIG9mIGxlZ2FjeSAiRU5WIGtleSB2YWx1ZSIgZm9ybWF0IChsaW5lIDIwKQ==",
"detail": [
"TGVnYWN5IGtleS92YWx1ZSBmb3JtYXQgd2l0aCB3aGl0ZXNwYWNlIHNlcGFyYXRvciBzaG91bGQgbm90IGJlIHVzZWQ="
],
"url": "https://docs.docker.com/go/dockerfile/rule/legacy-key-value-format/",
"sourceInfo": {
"filename": "services.dockerfile",
"data": "RlJPTSBub2RlOjIyLjEzLjAtc2xpbQoKUlVOIGFwdC1nZXQgdXBkYXRlICYmIGFwdC1nZXQgaW5zdGFsbCAteSB3Z2V0IGNhLWNlcnRpZmljYXRlcyAmJiBybSAtcmYgL3Zhci9saWIvYXB0L2xpc3RzLyoKCkFSRyBTRVJWSUNFX0RJUl9OQU1FCldPUktESVIgL3Vzci9zcmMvYXBwLyRTRVJWSUNFX0RJUl9OQU1FCgpDT1BZIC0tZnJvbT1kaXN0IC4gL3Vzci9zcmMvYXBwLyRTRVJWSUNFX0RJUl9OQU1FLwpDT1BZIC0tZnJvbT1zaGFyZWQgLiAvCgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UubGljZW5zZXM9TUlUCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS50aXRsZT0kSU1BR0VfVElUTEUKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnZlcnNpb249JFJFTEVBU0UKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmRlc2NyaXB0aW9uPSRJTUFHRV9ERVNDUklQVElPTgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UuYXV0aG9ycz0iVGhlIEd1aWxkIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudmVuZG9yPSJLYW1pbCBLaXNpZWxhIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudXJsPSJodHRwczovL2dpdGh1Yi5jb20vZ3JhcGhxbC1oaXZlL3BsYXRmb3JtIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2Uuc291cmNlPSJodHRwczovL2dpdGh1Yi5jb20vZ3JhcGhxbC1oaXZlL3BsYXRmb3JtIgoKRU5WIEVOVklST05NRU5UIHByb2R1Y3Rpb24KRU5WIFJFTEVBU0UgJFJFTEVBU0UKRU5WIFBPUlQgJFBPUlQKCkhFQUxUSENIRUNLIC0taW50ZXJ2YWw9NXMgXAogIC0tdGltZW91dD01cyBcCiAgLS1zdGFydC1wZXJpb2Q9NXMgXAogIC0tcmV0cmllcz02IFwKICBDTUQgJEhFQUxUSENIRUNLX0NNRAoKRU5UUllQT0lOVCBbICIvZW50cnlwb2ludC5zaCIgXQo=",
"language": "Dockerfile"
},
"range": [
{
"start": {
"line": 20
},
"end": {
"line": 20
}
}
]
},
{
"vertex": "sha256:4b57f193ffa97fd94332140f2aaed15c3281844a18761dd2750125c0906b7440",
"level": 1,
"short": "TGVnYWN5S2V5VmFsdWVGb3JtYXQ6ICJFTlYga2V5PXZhbHVlIiBzaG91bGQgYmUgdXNlZCBpbnN0ZWFkIG9mIGxlZ2FjeSAiRU5WIGtleSB2YWx1ZSIgZm9ybWF0IChsaW5lIDEwKQ==",
"detail": [
"TGVnYWN5IGtleS92YWx1ZSBmb3JtYXQgd2l0aCB3aGl0ZXNwYWNlIHNlcGFyYXRvciBzaG91bGQgbm90IGJlIHVzZWQ="
],
"url": "https://docs.docker.com/go/dockerfile/rule/legacy-key-value-format/",
"sourceInfo": {
"filename": "migrations.dockerfile",
"data": "RlJPTSBub2RlOjIyLjEzLjAtc2xpbQoKUlVOIGFwdC1nZXQgdXBkYXRlICYmIGFwdC1nZXQgaW5zdGFsbCAteSBjYS1jZXJ0aWZpY2F0ZXMKCldPUktESVIgL3Vzci9zcmMvYXBwCgpDT1BZIC0tZnJvbT1kaXN0IC4gL3Vzci9zcmMvYXBwLwpDT1BZIC0tZnJvbT1zaGFyZWQgLiAvCgpFTlYgRU5WSVJPTk1FTlQgcHJvZHVjdGlvbgpFTlYgTk9ERV9FTlYgcHJvZHVjdGlvbgpFTlYgUkVMRUFTRSAkUkVMRUFTRQoKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmxpY2Vuc2VzPU1JVApMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudGl0bGU9JElNQUdFX1RJVExFCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS52ZXJzaW9uPSRSRUxFQVNFCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS5kZXNjcmlwdGlvbj0kSU1BR0VfREVTQ1JJUFRJT04KTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmF1dGhvcnM9IlRoZSBHdWlsZCIKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnZlbmRvcj0iS2FtaWwgS2lzaWVsYSIKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnVybD0iaHR0cHM6Ly9naXRodWIuY29tL2dyYXBocWwtaGl2ZS9wbGF0Zm9ybSIKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnNvdXJjZT0iaHR0cHM6Ly9naXRodWIuY29tL2dyYXBocWwtaGl2ZS9wbGF0Zm9ybSIKCkVOVFJZUE9JTlQgWyAiL2VudHJ5cG9pbnQuc2giIF0K",
"language": "Dockerfile"
},
"range": [
{
"start": {
"line": 10
},
"end": {
"line": 10
}
}
]
},
{
"vertex": "sha256:8af84fb6a63a5ad58d473eaaed0553f175b34092dc843d96f59407ac19497b67",
"level": 1,
"short": "VW5kZWZpbmVkVmFyOiBVc2FnZSBvZiB1bmRlZmluZWQgdmFyaWFibGUgJyRJTUFHRV9USVRMRScgKGxpbmUgMTIp",
"detail": [
"VmFyaWFibGVzIHNob3VsZCBiZSBkZWZpbmVkIGJlZm9yZSB0aGVpciB1c2U="
],
"url": "https://docs.docker.com/go/dockerfile/rule/undefined-var/",
"sourceInfo": {
"filename": "services.dockerfile",
"data": "RlJPTSBub2RlOjIyLjEzLjAtc2xpbQoKUlVOIGFwdC1nZXQgdXBkYXRlICYmIGFwdC1nZXQgaW5zdGFsbCAteSB3Z2V0IGNhLWNlcnRpZmljYXRlcyAmJiBybSAtcmYgL3Zhci9saWIvYXB0L2xpc3RzLyoKCkFSRyBTRVJWSUNFX0RJUl9OQU1FCldPUktESVIgL3Vzci9zcmMvYXBwLyRTRVJWSUNFX0RJUl9OQU1FCgpDT1BZIC0tZnJvbT1kaXN0IC4gL3Vzci9zcmMvYXBwLyRTRVJWSUNFX0RJUl9OQU1FLwpDT1BZIC0tZnJvbT1zaGFyZWQgLiAvCgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UubGljZW5zZXM9TUlUCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS50aXRsZT0kSU1BR0VfVElUTEUKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnZlcnNpb249JFJFTEVBU0UKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmRlc2NyaXB0aW9uPSRJTUFHRV9ERVNDUklQVElPTgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UuYXV0aG9ycz0iVGhlIEd1aWxkIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudmVuZG9yPSJLYW1pbCBLaXNpZWxhIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudXJsPSJodHRwczovL2dpdGh1Yi5jb20vZ3JhcGhxbC1oaXZlL3BsYXRmb3JtIgpMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2Uuc291cmNlPSJodHRwczovL2dpdGh1Yi5jb20vZ3JhcGhxbC1oaXZlL3BsYXRmb3JtIgoKRU5WIEVOVklST05NRU5UIHByb2R1Y3Rpb24KRU5WIFJFTEVBU0UgJFJFTEVBU0UKRU5WIFBPUlQgJFBPUlQKCkhFQUxUSENIRUNLIC0taW50ZXJ2YWw9NXMgXAogIC0tdGltZW91dD01cyBcCiAgLS1zdGFydC1wZXJpb2Q9NXMgXAogIC0tcmV0cmllcz02IFwKICBDTUQgJEhFQUxUSENIRUNLX0NNRAoKRU5UUllQT0lOVCBbICIvZW50cnlwb2ludC5zaCIgXQo=",
"language": "Dockerfile"
},
"range": [
{
"start": {
"line": 12
},
"end": {
"line": 12
}
}
]
},
{
"vertex": "sha256:4b57f193ffa97fd94332140f2aaed15c3281844a18761dd2750125c0906b7440",
"level": 1,
"short": "VW5kZWZpbmVkVmFyOiBVc2FnZSBvZiB1bmRlZmluZWQgdmFyaWFibGUgJyRSRUxFQVNFJyAobGluZSAxMik=",
"detail": [
"VmFyaWFibGVzIHNob3VsZCBiZSBkZWZpbmVkIGJlZm9yZSB0aGVpciB1c2U="
],
"url": "https://docs.docker.com/go/dockerfile/rule/undefined-var/",
"sourceInfo": {
"filename": "migrations.dockerfile",
"data": "RlJPTSBub2RlOjIyLjEzLjAtc2xpbQoKUlVOIGFwdC1nZXQgdXBkYXRlICYmIGFwdC1nZXQgaW5zdGFsbCAteSBjYS1jZXJ0aWZpY2F0ZXMKCldPUktESVIgL3Vzci9zcmMvYXBwCgpDT1BZIC0tZnJvbT1kaXN0IC4gL3Vzci9zcmMvYXBwLwpDT1BZIC0tZnJvbT1zaGFyZWQgLiAvCgpFTlYgRU5WSVJPTk1FTlQgcHJvZHVjdGlvbgpFTlYgTk9ERV9FTlYgcHJvZHVjdGlvbgpFTlYgUkVMRUFTRSAkUkVMRUFTRQoKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmxpY2Vuc2VzPU1JVApMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudGl0bGU9JElNQUdFX1RJVExFCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS52ZXJzaW9uPSRSRUxFQVNFCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS5kZXNjcmlwdGlvbj0kSU1BR0VfREVTQ1JJUFRJT04KTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmF1dGhvcnM9IlRoZSBHdWlsZCIKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnZlbmRvcj0iS2FtaWwgS2lzaWVsYSIKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnVybD0iaHR0cHM6Ly9naXRodWIuY29tL2dyYXBocWwtaGl2ZS9wbGF0Zm9ybSIKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnNvdXJjZT0iaHR0cHM6Ly9naXRodWIuY29tL2dyYXBocWwtaGl2ZS9wbGF0Zm9ybSIKCkVOVFJZUE9JTlQgWyAiL2VudHJ5cG9pbnQuc2giIF0K",
"language": "Dockerfile"
},
"range": [
{
"start": {
"line": 12
},
"end": {
"line": 12
}
}
]
},
{
"vertex": "sha256:4b57f193ffa97fd94332140f2aaed15c3281844a18761dd2750125c0906b7440",
"level": 1,
"short": "TGVnYWN5S2V5VmFsdWVGb3JtYXQ6ICJFTlYga2V5PXZhbHVlIiBzaG91bGQgYmUgdXNlZCBpbnN0ZWFkIG9mIGxlZ2FjeSAiRU5WIGtleSB2YWx1ZSIgZm9ybWF0IChsaW5lIDEyKQ==",
"detail": [
"TGVnYWN5IGtleS92YWx1ZSBmb3JtYXQgd2l0aCB3aGl0ZXNwYWNlIHNlcGFyYXRvciBzaG91bGQgbm90IGJlIHVzZWQ="
],
"url": "https://docs.docker.com/go/dockerfile/rule/legacy-key-value-format/",
"sourceInfo": {
"filename": "migrations.dockerfile",
"data": "RlJPTSBub2RlOjIyLjEzLjAtc2xpbQoKUlVOIGFwdC1nZXQgdXBkYXRlICYmIGFwdC1nZXQgaW5zdGFsbCAteSBjYS1jZXJ0aWZpY2F0ZXMKCldPUktESVIgL3Vzci9zcmMvYXBwCgpDT1BZIC0tZnJvbT1kaXN0IC4gL3Vzci9zcmMvYXBwLwpDT1BZIC0tZnJvbT1zaGFyZWQgLiAvCgpFTlYgRU5WSVJPTk1FTlQgcHJvZHVjdGlvbgpFTlYgTk9ERV9FTlYgcHJvZHVjdGlvbgpFTlYgUkVMRUFTRSAkUkVMRUFTRQoKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmxpY2Vuc2VzPU1JVApMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudGl0bGU9JElNQUdFX1RJVExFCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS52ZXJzaW9uPSRSRUxFQVNFCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS5kZXNjcmlwdGlvbj0kSU1BR0VfREVTQ1JJUFRJT04KTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmF1dGhvcnM9IlRoZSBHdWlsZCIKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnZlbmRvcj0iS2FtaWwgS2lzaWVsYSIKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnVybD0iaHR0cHM6Ly9naXRodWIuY29tL2dyYXBocWwtaGl2ZS9wbGF0Zm9ybSIKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnNvdXJjZT0iaHR0cHM6Ly9naXRodWIuY29tL2dyYXBocWwtaGl2ZS9wbGF0Zm9ybSIKCkVOVFJZUE9JTlQgWyAiL2VudHJ5cG9pbnQuc2giIF0K",
"language": "Dockerfile"
},
"range": [
{
"start": {
"line": 12
},
"end": {
"line": 12
}
}
]
},
{
"vertex": "sha256:4b57f193ffa97fd94332140f2aaed15c3281844a18761dd2750125c0906b7440",
"level": 1,
"short": "VW5kZWZpbmVkVmFyOiBVc2FnZSBvZiB1bmRlZmluZWQgdmFyaWFibGUgJyRJTUFHRV9USVRMRScgKGxpbmUgMTUp",
"detail": [
"VmFyaWFibGVzIHNob3VsZCBiZSBkZWZpbmVkIGJlZm9yZSB0aGVpciB1c2U="
],
"url": "https://docs.docker.com/go/dockerfile/rule/undefined-var/",
"sourceInfo": {
"filename": "migrations.dockerfile",
"data": "RlJPTSBub2RlOjIyLjEzLjAtc2xpbQoKUlVOIGFwdC1nZXQgdXBkYXRlICYmIGFwdC1nZXQgaW5zdGFsbCAteSBjYS1jZXJ0aWZpY2F0ZXMKCldPUktESVIgL3Vzci9zcmMvYXBwCgpDT1BZIC0tZnJvbT1kaXN0IC4gL3Vzci9zcmMvYXBwLwpDT1BZIC0tZnJvbT1zaGFyZWQgLiAvCgpFTlYgRU5WSVJPTk1FTlQgcHJvZHVjdGlvbgpFTlYgTk9ERV9FTlYgcHJvZHVjdGlvbgpFTlYgUkVMRUFTRSAkUkVMRUFTRQoKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmxpY2Vuc2VzPU1JVApMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudGl0bGU9JElNQUdFX1RJVExFCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS52ZXJzaW9uPSRSRUxFQVNFCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS5kZXNjcmlwdGlvbj0kSU1BR0VfREVTQ1JJUFRJT04KTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmF1dGhvcnM9IlRoZSBHdWlsZCIKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnZlbmRvcj0iS2FtaWwgS2lzaWVsYSIKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnVybD0iaHR0cHM6Ly9naXRodWIuY29tL2dyYXBocWwtaGl2ZS9wbGF0Zm9ybSIKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnNvdXJjZT0iaHR0cHM6Ly9naXRodWIuY29tL2dyYXBocWwtaGl2ZS9wbGF0Zm9ybSIKCkVOVFJZUE9JTlQgWyAiL2VudHJ5cG9pbnQuc2giIF0K",
"language": "Dockerfile"
},
"range": [
{
"start": {
"line": 15
},
"end": {
"line": 15
}
}
]
},
{
"vertex": "sha256:4b57f193ffa97fd94332140f2aaed15c3281844a18761dd2750125c0906b7440",
"level": 1,
"short": "VW5kZWZpbmVkVmFyOiBVc2FnZSBvZiB1bmRlZmluZWQgdmFyaWFibGUgJyRJTUFHRV9ERVNDUklQVElPTicgKGxpbmUgMTcp",
"detail": [
"VmFyaWFibGVzIHNob3VsZCBiZSBkZWZpbmVkIGJlZm9yZSB0aGVpciB1c2U="
],
"url": "https://docs.docker.com/go/dockerfile/rule/undefined-var/",
"sourceInfo": {
"filename": "migrations.dockerfile",
"data": "RlJPTSBub2RlOjIyLjEzLjAtc2xpbQoKUlVOIGFwdC1nZXQgdXBkYXRlICYmIGFwdC1nZXQgaW5zdGFsbCAteSBjYS1jZXJ0aWZpY2F0ZXMKCldPUktESVIgL3Vzci9zcmMvYXBwCgpDT1BZIC0tZnJvbT1kaXN0IC4gL3Vzci9zcmMvYXBwLwpDT1BZIC0tZnJvbT1zaGFyZWQgLiAvCgpFTlYgRU5WSVJPTk1FTlQgcHJvZHVjdGlvbgpFTlYgTk9ERV9FTlYgcHJvZHVjdGlvbgpFTlYgUkVMRUFTRSAkUkVMRUFTRQoKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmxpY2Vuc2VzPU1JVApMQUJFTCBvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UudGl0bGU9JElNQUdFX1RJVExFCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS52ZXJzaW9uPSRSRUxFQVNFCkxBQkVMIG9yZy5vcGVuY29udGFpbmVycy5pbWFnZS5kZXNjcmlwdGlvbj0kSU1BR0VfREVTQ1JJUFRJT04KTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLmF1dGhvcnM9IlRoZSBHdWlsZCIKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnZlbmRvcj0iS2FtaWwgS2lzaWVsYSIKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnVybD0iaHR0cHM6Ly9naXRodWIuY29tL2dyYXBocWwtaGl2ZS9wbGF0Zm9ybSIKTEFCRUwgb3JnLm9wZW5jb250YWluZXJzLmltYWdlLnNvdXJjZT0iaHR0cHM6Ly9naXRodWIuY29tL2dyYXBocWwtaGl2ZS9wbGF0Zm9ybSIKCkVOVFJZUE9JTlQgWyAiL2VudHJ5cG9pbnQuc2giIF0K",
"language": "Dockerfile"
},
"range": [
{
"start": {
"line": 17
},
"end": {
"line": 17
}
}
]
}
],
"composition-federation-2": {
"buildx.build.provenance": {
"buildType": "https://mobyproject.org/buildkit@v1",
"materials": [
{
"uri": "pkg:docker/[email protected]?platform=linux%2Famd64",
"digest": {
"sha256": "f5a0871ab03b035c58bdb3007c3d177b001c2145c18e81817b71624dcf7d8bff"
}
}
],
"invocation": {
"configSource": {
"entryPoint": "services.dockerfile"
},
"parameters": {
"frontend": "dockerfile.v0",
"args": {
"build-arg:HEALTHCHECK_CMD": "wget --spider -q http://127.0.0.1:${PORT}/_readiness",
"build-arg:IMAGE_DESCRIPTION": "Federation 2 Composition Service for GraphQL Hive.",
"build-arg:IMAGE_TITLE": "graphql-hive/composition-federation-2",
"build-arg:PORT": "3069",
"build-arg:RELEASE": "d06ba598fd8ecc3a7d621a9877229a596f581d55",
"build-arg:SERVICE_DIR_NAME": "@hive/external-composition",
"context:dist": "local:dist",
"context:shared": "local:shared",
"frontend.caps": "moby.buildkit.frontend.contexts+forward",
"local-sessionid:context": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:dockerfile": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:shared": "ojjxc7lh7rxj7w075fb9rgdfc"
},
"locals": [
{
"name": "context"
},
{
"name": "dist"
},
{
"name": "dockerfile"
},
{
"name": "shared"
}
]
},
"environment": {
"platform": "linux/amd64"
}
}
},
"buildx.build.ref": "builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c6/builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c60/0i520jvchfs52hm3zrm2422wr",
"containerimage.config.digest": "sha256:11428f0f829a8d8a2d567f2e68fa6b7e5dcaa96623d6f95f33dc9e6ba62afff4",
"containerimage.descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:21af77976e626f6e0c80b3d9b134dc194b7446407d4749179bae3a067bde98dc",
"size": 2075,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
"containerimage.digest": "sha256:21af77976e626f6e0c80b3d9b134dc194b7446407d4749179bae3a067bde98dc",
"image.name": "ghcr.io/graphql-hive/composition-federation-2:d06ba598fd8ecc3a7d621a9877229a596f581d55-amd64,ghcr.io/graphql-hive/composition-federation-2:feat_permission_member_roles-amd64"
},
"emails": {
"buildx.build.provenance": {
"buildType": "https://mobyproject.org/buildkit@v1",
"materials": [
{
"uri": "pkg:docker/[email protected]?platform=linux%2Famd64",
"digest": {
"sha256": "f5a0871ab03b035c58bdb3007c3d177b001c2145c18e81817b71624dcf7d8bff"
}
}
],
"invocation": {
"configSource": {
"entryPoint": "services.dockerfile"
},
"parameters": {
"frontend": "dockerfile.v0",
"args": {
"build-arg:HEALTHCHECK_CMD": "wget --spider -q http://127.0.0.1:${PORT}/_readiness",
"build-arg:IMAGE_DESCRIPTION": "The emails service of the GraphQL Hive project.",
"build-arg:IMAGE_TITLE": "graphql-hive/emails",
"build-arg:PORT": "3006",
"build-arg:RELEASE": "d06ba598fd8ecc3a7d621a9877229a596f581d55",
"build-arg:SERVICE_DIR_NAME": "@hive/emails",
"context:dist": "local:dist",
"context:shared": "local:shared",
"frontend.caps": "moby.buildkit.frontend.contexts+forward",
"local-sessionid:context": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:dockerfile": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:shared": "ojjxc7lh7rxj7w075fb9rgdfc"
},
"locals": [
{
"name": "context"
},
{
"name": "dist"
},
{
"name": "dockerfile"
},
{
"name": "shared"
}
]
},
"environment": {
"platform": "linux/amd64"
}
}
},
"buildx.build.ref": "builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c6/builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c60/aytmb2ri4fjplqns8io001bi5",
"containerimage.config.digest": "sha256:c28330aa63b0696a9abfbe7152e30fd3649a767132ff2f262551d9d0df9cc4c2",
"containerimage.descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:d6a2777d527ac03bc187138fa2baa37e7562013faec14c3679879fa06dc2b6e2",
"size": 2075,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
"containerimage.digest": "sha256:d6a2777d527ac03bc187138fa2baa37e7562013faec14c3679879fa06dc2b6e2",
"image.name": "ghcr.io/graphql-hive/emails:d06ba598fd8ecc3a7d621a9877229a596f581d55-amd64,ghcr.io/graphql-hive/emails:feat_permission_member_roles-amd64"
},
"policy": {
"buildx.build.provenance": {
"buildType": "https://mobyproject.org/buildkit@v1",
"materials": [
{
"uri": "pkg:docker/[email protected]?platform=linux%2Famd64",
"digest": {
"sha256": "f5a0871ab03b035c58bdb3007c3d177b001c2145c18e81817b71624dcf7d8bff"
}
}
],
"invocation": {
"configSource": {
"entryPoint": "services.dockerfile"
},
"parameters": {
"frontend": "dockerfile.v0",
"args": {
"build-arg:HEALTHCHECK_CMD": "wget --spider -q http://127.0.0.1:${PORT}/_readiness",
"build-arg:IMAGE_DESCRIPTION": "The policy service of the GraphQL Hive project.",
"build-arg:IMAGE_TITLE": "graphql-hive/policy",
"build-arg:PORT": "3012",
"build-arg:RELEASE": "d06ba598fd8ecc3a7d621a9877229a596f581d55",
"build-arg:SERVICE_DIR_NAME": "@hive/policy",
"context:dist": "local:dist",
"context:shared": "local:shared",
"frontend.caps": "moby.buildkit.frontend.contexts+forward",
"local-sessionid:context": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:dockerfile": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:shared": "ojjxc7lh7rxj7w075fb9rgdfc"
},
"locals": [
{
"name": "context"
},
{
"name": "dist"
},
{
"name": "dockerfile"
},
{
"name": "shared"
}
]
},
"environment": {
"platform": "linux/amd64"
}
}
},
"buildx.build.ref": "builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c6/builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c60/j9917sz9slzeopncy339e54dt",
"containerimage.config.digest": "sha256:cdd057e83aa7a5f78fec5bee0bef402ae81d09c444c24c017bd7a08e86c42713",
"containerimage.descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:9380fe31a69bbd89376e5360544d6608c36fe3d9a8b816c8032b448ae4230224",
"size": 2075,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
"containerimage.digest": "sha256:9380fe31a69bbd89376e5360544d6608c36fe3d9a8b816c8032b448ae4230224",
"image.name": "ghcr.io/graphql-hive/policy:d06ba598fd8ecc3a7d621a9877229a596f581d55-amd64,ghcr.io/graphql-hive/policy:feat_permission_member_roles-amd64"
},
"rate-limit": {
"buildx.build.provenance": {
"buildType": "https://mobyproject.org/buildkit@v1",
"materials": [
{
"uri": "pkg:docker/[email protected]?platform=linux%2Famd64",
"digest": {
"sha256": "f5a0871ab03b035c58bdb3007c3d177b001c2145c18e81817b71624dcf7d8bff"
}
}
],
"invocation": {
"configSource": {
"entryPoint": "services.dockerfile"
},
"parameters": {
"frontend": "dockerfile.v0",
"args": {
"build-arg:HEALTHCHECK_CMD": "wget --spider -q http://127.0.0.1:${PORT}/_readiness",
"build-arg:IMAGE_DESCRIPTION": "The rate limit service of the GraphQL Hive project.",
"build-arg:IMAGE_TITLE": "graphql-hive/rate-limit",
"build-arg:PORT": "3009",
"build-arg:RELEASE": "d06ba598fd8ecc3a7d621a9877229a596f581d55",
"build-arg:SERVICE_DIR_NAME": "@hive/rate-limit",
"context:dist": "local:dist",
"context:shared": "local:shared",
"frontend.caps": "moby.buildkit.frontend.contexts+forward",
"local-sessionid:context": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:dockerfile": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:shared": "ojjxc7lh7rxj7w075fb9rgdfc"
},
"locals": [
{
"name": "context"
},
{
"name": "dist"
},
{
"name": "dockerfile"
},
{
"name": "shared"
}
]
},
"environment": {
"platform": "linux/amd64"
}
}
},
"buildx.build.ref": "builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c6/builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c60/f3v5nq44lttwwml2tmzu2g8g8",
"containerimage.config.digest": "sha256:3d5eb61d690bcc3058f6bf8f91d147547c40ca10394190f4f2ca67b9522bba34",
"containerimage.descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:94644f1801a14412c46869d2a2f8573dc1f28aa3123a9a1ef3defef91fc4ecef",
"size": 2075,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
"containerimage.digest": "sha256:94644f1801a14412c46869d2a2f8573dc1f28aa3123a9a1ef3defef91fc4ecef",
"image.name": "ghcr.io/graphql-hive/rate-limit:d06ba598fd8ecc3a7d621a9877229a596f581d55-amd64,ghcr.io/graphql-hive/rate-limit:feat_permission_member_roles-amd64"
},
"schema": {
"buildx.build.provenance": {
"buildType": "https://mobyproject.org/buildkit@v1",
"materials": [
{
"uri": "pkg:docker/[email protected]?platform=linux%2Famd64",
"digest": {
"sha256": "f5a0871ab03b035c58bdb3007c3d177b001c2145c18e81817b71624dcf7d8bff"
}
}
],
"invocation": {
"configSource": {
"entryPoint": "services.dockerfile"
},
"parameters": {
"frontend": "dockerfile.v0",
"args": {
"build-arg:HEALTHCHECK_CMD": "wget --spider -q http://127.0.0.1:${PORT}/_readiness",
"build-arg:IMAGE_DESCRIPTION": "The schema service of the GraphQL Hive project.",
"build-arg:IMAGE_TITLE": "graphql-hive/schema",
"build-arg:PORT": "3002",
"build-arg:RELEASE": "d06ba598fd8ecc3a7d621a9877229a596f581d55",
"build-arg:SERVICE_DIR_NAME": "@hive/schema",
"context:dist": "local:dist",
"context:shared": "local:shared",
"frontend.caps": "moby.buildkit.frontend.contexts+forward",
"local-sessionid:context": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:dockerfile": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:shared": "ojjxc7lh7rxj7w075fb9rgdfc"
},
"locals": [
{
"name": "context"
},
{
"name": "dist"
},
{
"name": "dockerfile"
},
{
"name": "shared"
}
]
},
"environment": {
"platform": "linux/amd64"
}
}
},
"buildx.build.ref": "builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c6/builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c60/2drxgel5fzw61m8iwjnkouco8",
"containerimage.config.digest": "sha256:1df8d1e4cf00305ea4df2913e4008d19e99913869682527ac5b2e5589ee6b1ac",
"containerimage.descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:97c264137fedc2f62c05a8a6faf8410a3d6022fd509e0047731aab21bb7d4dfd",
"size": 2075,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
"containerimage.digest": "sha256:97c264137fedc2f62c05a8a6faf8410a3d6022fd509e0047731aab21bb7d4dfd",
"image.name": "ghcr.io/graphql-hive/schema:d06ba598fd8ecc3a7d621a9877229a596f581d55-amd64,ghcr.io/graphql-hive/schema:feat_permission_member_roles-amd64"
},
"server": {
"buildx.build.provenance": {
"buildType": "https://mobyproject.org/buildkit@v1",
"materials": [
{
"uri": "pkg:docker/[email protected]?platform=linux%2Famd64",
"digest": {
"sha256": "f5a0871ab03b035c58bdb3007c3d177b001c2145c18e81817b71624dcf7d8bff"
}
}
],
"invocation": {
"configSource": {
"entryPoint": "services.dockerfile"
},
"parameters": {
"frontend": "dockerfile.v0",
"args": {
"build-arg:HEALTHCHECK_CMD": "wget --spider -q http://127.0.0.1:${PORT}/_readiness",
"build-arg:IMAGE_DESCRIPTION": "The server service of the GraphQL Hive project.",
"build-arg:IMAGE_TITLE": "graphql-hive/server",
"build-arg:PORT": "3001",
"build-arg:RELEASE": "d06ba598fd8ecc3a7d621a9877229a596f581d55",
"build-arg:SERVICE_DIR_NAME": "@hive/server",
"context:dist": "local:dist",
"context:shared": "local:shared",
"frontend.caps": "moby.buildkit.frontend.contexts+forward",
"local-sessionid:context": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:dockerfile": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:shared": "ojjxc7lh7rxj7w075fb9rgdfc"
},
"locals": [
{
"name": "context"
},
{
"name": "dist"
},
{
"name": "dockerfile"
},
{
"name": "shared"
}
]
},
"environment": {
"platform": "linux/amd64"
}
}
},
"buildx.build.ref": "builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c6/builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c60/x9ozzihbm5saly1zdbj896fzz",
"containerimage.config.digest": "sha256:598e9b545c4b5c5dd5fd6631f38f56ab8c5513a592721ad10e64826d8144e300",
"containerimage.descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:ec31c819876b380f49353d88dba16899cc30b801c00c0736fa8739b795a0ed4e",
"size": 2076,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
"containerimage.digest": "sha256:ec31c819876b380f49353d88dba16899cc30b801c00c0736fa8739b795a0ed4e",
"image.name": "ghcr.io/graphql-hive/server:d06ba598fd8ecc3a7d621a9877229a596f581d55-amd64,ghcr.io/graphql-hive/server:feat_permission_member_roles-amd64"
},
"storage": {
"buildx.build.provenance": {
"buildType": "https://mobyproject.org/buildkit@v1",
"materials": [
{
"uri": "pkg:docker/[email protected]?platform=linux%2Famd64",
"digest": {
"sha256": "f5a0871ab03b035c58bdb3007c3d177b001c2145c18e81817b71624dcf7d8bff"
}
}
],
"invocation": {
"configSource": {
"entryPoint": "migrations.dockerfile"
},
"parameters": {
"frontend": "dockerfile.v0",
"args": {
"build-arg:IMAGE_DESCRIPTION": "The migrations service of the GraphQL Hive project.",
"build-arg:IMAGE_TITLE": "graphql-hive/storage",
"build-arg:RELEASE": "d06ba598fd8ecc3a7d621a9877229a596f581d55",
"context:dist": "local:dist",
"context:shared": "local:shared",
"frontend.caps": "moby.buildkit.frontend.contexts+forward",
"local-sessionid:context": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:dockerfile": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:shared": "ojjxc7lh7rxj7w075fb9rgdfc"
},
"locals": [
{
"name": "context"
},
{
"name": "dist"
},
{
"name": "dockerfile"
},
{
"name": "shared"
}
]
},
"environment": {
"platform": "linux/amd64"
}
}
},
"buildx.build.ref": "builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c6/builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c60/keeq095ohhgvkkfn99n5m1dtu",
"containerimage.config.digest": "sha256:5fc3137c80a3a4be241f6a0d939a373ba7396967996698e17f8e17e49db099bf",
"containerimage.descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:7763f287e0dce9611a7d067889dcd64706efe96fc9528c96cad640e2bd0098de",
"size": 2075,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
"containerimage.digest": "sha256:7763f287e0dce9611a7d067889dcd64706efe96fc9528c96cad640e2bd0098de",
"image.name": "ghcr.io/graphql-hive/storage:d06ba598fd8ecc3a7d621a9877229a596f581d55-amd64,ghcr.io/graphql-hive/storage:feat_permission_member_roles-amd64"
},
"stripe-billing": {
"buildx.build.provenance": {
"buildType": "https://mobyproject.org/buildkit@v1",
"materials": [
{
"uri": "pkg:docker/[email protected]?platform=linux%2Famd64",
"digest": {
"sha256": "f5a0871ab03b035c58bdb3007c3d177b001c2145c18e81817b71624dcf7d8bff"
}
}
],
"invocation": {
"configSource": {
"entryPoint": "services.dockerfile"
},
"parameters": {
"frontend": "dockerfile.v0",
"args": {
"build-arg:HEALTHCHECK_CMD": "wget --spider -q http://127.0.0.1:${PORT}/_readiness",
"build-arg:IMAGE_DESCRIPTION": "The stripe billing service of the GraphQL Hive project.",
"build-arg:IMAGE_TITLE": "graphql-hive/stripe-billing",
"build-arg:PORT": "3010",
"build-arg:RELEASE": "d06ba598fd8ecc3a7d621a9877229a596f581d55",
"build-arg:SERVICE_DIR_NAME": "@hive/stripe-billing",
"context:dist": "local:dist",
"context:shared": "local:shared",
"frontend.caps": "moby.buildkit.frontend.contexts+forward",
"local-sessionid:context": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:dockerfile": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:shared": "ojjxc7lh7rxj7w075fb9rgdfc"
},
"locals": [
{
"name": "context"
},
{
"name": "dist"
},
{
"name": "dockerfile"
},
{
"name": "shared"
}
]
},
"environment": {
"platform": "linux/amd64"
}
}
},
"buildx.build.ref": "builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c6/builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c60/b1mr5s3dafxsuepmxwquwokig",
"containerimage.config.digest": "sha256:f9435d1483e225c5cf77605db637342092007fc79291183ecff2631e17f939b1",
"containerimage.descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:9fc3698d1ad2da081c8ce9de99d8010647b0bd751cf8cf190c1108d525feb0e2",
"size": 2075,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
"containerimage.digest": "sha256:9fc3698d1ad2da081c8ce9de99d8010647b0bd751cf8cf190c1108d525feb0e2",
"image.name": "ghcr.io/graphql-hive/stripe-billing:d06ba598fd8ecc3a7d621a9877229a596f581d55-amd64,ghcr.io/graphql-hive/stripe-billing:feat_permission_member_roles-amd64"
},
"tokens": {
"buildx.build.provenance": {
"buildType": "https://mobyproject.org/buildkit@v1",
"materials": [
{
"uri": "pkg:docker/[email protected]?platform=linux%2Famd64",
"digest": {
"sha256": "f5a0871ab03b035c58bdb3007c3d177b001c2145c18e81817b71624dcf7d8bff"
}
}
],
"invocation": {
"configSource": {
"entryPoint": "services.dockerfile"
},
"parameters": {
"frontend": "dockerfile.v0",
"args": {
"build-arg:HEALTHCHECK_CMD": "wget --spider -q http://127.0.0.1:${PORT}/_readiness",
"build-arg:IMAGE_DESCRIPTION": "The tokens service of the GraphQL Hive project.",
"build-arg:IMAGE_TITLE": "graphql-hive/tokens",
"build-arg:PORT": "3003",
"build-arg:RELEASE": "d06ba598fd8ecc3a7d621a9877229a596f581d55",
"build-arg:SERVICE_DIR_NAME": "@hive/tokens",
"context:dist": "local:dist",
"context:shared": "local:shared",
"frontend.caps": "moby.buildkit.frontend.contexts+forward",
"local-sessionid:context": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:dockerfile": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:shared": "ojjxc7lh7rxj7w075fb9rgdfc"
},
"locals": [
{
"name": "context"
},
{
"name": "dist"
},
{
"name": "dockerfile"
},
{
"name": "shared"
}
]
},
"environment": {
"platform": "linux/amd64"
}
}
},
"buildx.build.ref": "builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c6/builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c60/eg53ftn3s5c0wbvwh26bvnt5h",
"containerimage.config.digest": "sha256:12fecfe60eb8867e148d5f3ab75b1972abc2dbc7e5a70baf3ac72a9c1d0f103a",
"containerimage.descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:995e9ef167facd5ec610671ec852eb03c8be79db51ff9aa93014c32f25707044",
"size": 2075,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
"containerimage.digest": "sha256:995e9ef167facd5ec610671ec852eb03c8be79db51ff9aa93014c32f25707044",
"image.name": "ghcr.io/graphql-hive/tokens:d06ba598fd8ecc3a7d621a9877229a596f581d55-amd64,ghcr.io/graphql-hive/tokens:feat_permission_member_roles-amd64"
},
"usage": {
"buildx.build.provenance": {
"buildType": "https://mobyproject.org/buildkit@v1",
"materials": [
{
"uri": "pkg:docker/[email protected]?platform=linux%2Famd64",
"digest": {
"sha256": "f5a0871ab03b035c58bdb3007c3d177b001c2145c18e81817b71624dcf7d8bff"
}
}
],
"invocation": {
"configSource": {
"entryPoint": "services.dockerfile"
},
"parameters": {
"frontend": "dockerfile.v0",
"args": {
"build-arg:HEALTHCHECK_CMD": "wget --spider -q http://127.0.0.1:${PORT}/_readiness",
"build-arg:IMAGE_DESCRIPTION": "The usage ingestor service of the GraphQL Hive project.",
"build-arg:IMAGE_TITLE": "graphql-hive/usage",
"build-arg:PORT": "3006",
"build-arg:RELEASE": "d06ba598fd8ecc3a7d621a9877229a596f581d55",
"build-arg:SERVICE_DIR_NAME": "@hive/usage",
"context:dist": "local:dist",
"context:shared": "local:shared",
"frontend.caps": "moby.buildkit.frontend.contexts+forward",
"local-sessionid:context": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:dockerfile": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:shared": "ojjxc7lh7rxj7w075fb9rgdfc"
},
"locals": [
{
"name": "context"
},
{
"name": "dist"
},
{
"name": "dockerfile"
},
{
"name": "shared"
}
]
},
"environment": {
"platform": "linux/amd64"
}
}
},
"buildx.build.ref": "builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c6/builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c60/ytw5vo6nyd0ym6w2pc0dxtbdj",
"containerimage.config.digest": "sha256:2098cdeaacf97df761bf79e5095188e0e31b256a417829a63174907c47f33e4c",
"containerimage.descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:27074402906bafce84ba7257a03da8cf39c370b632e13dcc454e4d3d51e138b8",
"size": 2075,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
"containerimage.digest": "sha256:27074402906bafce84ba7257a03da8cf39c370b632e13dcc454e4d3d51e138b8",
"image.name": "ghcr.io/graphql-hive/usage:d06ba598fd8ecc3a7d621a9877229a596f581d55-amd64,ghcr.io/graphql-hive/usage:feat_permission_member_roles-amd64"
},
"usage-estimator": {
"buildx.build.provenance": {
"buildType": "https://mobyproject.org/buildkit@v1",
"materials": [
{
"uri": "pkg:docker/[email protected]?platform=linux%2Famd64",
"digest": {
"sha256": "f5a0871ab03b035c58bdb3007c3d177b001c2145c18e81817b71624dcf7d8bff"
}
}
],
"invocation": {
"configSource": {
"entryPoint": "services.dockerfile"
},
"parameters": {
"frontend": "dockerfile.v0",
"args": {
"build-arg:HEALTHCHECK_CMD": "wget --spider -q http://127.0.0.1:${PORT}/_readiness",
"build-arg:IMAGE_DESCRIPTION": "The usage estimator service of the GraphQL Hive project.",
"build-arg:IMAGE_TITLE": "graphql-hive/usage-estimator",
"build-arg:PORT": "3008",
"build-arg:RELEASE": "d06ba598fd8ecc3a7d621a9877229a596f581d55",
"build-arg:SERVICE_DIR_NAME": "@hive/usage-estimator",
"context:dist": "local:dist",
"context:shared": "local:shared",
"frontend.caps": "moby.buildkit.frontend.contexts+forward",
"local-sessionid:context": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:dockerfile": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:shared": "ojjxc7lh7rxj7w075fb9rgdfc"
},
"locals": [
{
"name": "context"
},
{
"name": "dist"
},
{
"name": "dockerfile"
},
{
"name": "shared"
}
]
},
"environment": {
"platform": "linux/amd64"
}
}
},
"buildx.build.ref": "builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c6/builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c60/yb8yzg014up175c953i0cajyr",
"containerimage.config.digest": "sha256:4e156afaff342f1b20607b4b776422cfe763cac034415edf32429e7ad6fa36fe",
"containerimage.descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:7afdcf481f76f9c60b2e9b250d572ff0af932aaf072491eeb5506ca949bd52dc",
"size": 2075,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
"containerimage.digest": "sha256:7afdcf481f76f9c60b2e9b250d572ff0af932aaf072491eeb5506ca949bd52dc",
"image.name": "ghcr.io/graphql-hive/usage-estimator:d06ba598fd8ecc3a7d621a9877229a596f581d55-amd64,ghcr.io/graphql-hive/usage-estimator:feat_permission_member_roles-amd64"
},
"usage-ingestor": {
"buildx.build.provenance": {
"buildType": "https://mobyproject.org/buildkit@v1",
"materials": [
{
"uri": "pkg:docker/[email protected]?platform=linux%2Famd64",
"digest": {
"sha256": "f5a0871ab03b035c58bdb3007c3d177b001c2145c18e81817b71624dcf7d8bff"
}
}
],
"invocation": {
"configSource": {
"entryPoint": "services.dockerfile"
},
"parameters": {
"frontend": "dockerfile.v0",
"args": {
"build-arg:HEALTHCHECK_CMD": "wget --spider -q http://127.0.0.1:${PORT}/_readiness",
"build-arg:IMAGE_DESCRIPTION": "The usage ingestor service of the GraphQL Hive project.",
"build-arg:IMAGE_TITLE": "graphql-hive/usage-ingestor",
"build-arg:PORT": "3007",
"build-arg:RELEASE": "d06ba598fd8ecc3a7d621a9877229a596f581d55",
"build-arg:SERVICE_DIR_NAME": "@hive/usage-ingestor",
"context:dist": "local:dist",
"context:shared": "local:shared",
"frontend.caps": "moby.buildkit.frontend.contexts+forward",
"local-sessionid:context": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:dockerfile": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:shared": "ojjxc7lh7rxj7w075fb9rgdfc"
},
"locals": [
{
"name": "context"
},
{
"name": "dist"
},
{
"name": "dockerfile"
},
{
"name": "shared"
}
]
},
"environment": {
"platform": "linux/amd64"
}
}
},
"buildx.build.ref": "builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c6/builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c60/ntlx629n2pjqaj0b430r1y8ap",
"containerimage.config.digest": "sha256:fead893a5dbf910d8da7778bccadc75147d83177c8bd03e83f3fdefa0f0f565e",
"containerimage.descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:f0b83dbc066f8e64481d13450dc3fca3ae13708f2d0046b034a2f46d1368242e",
"size": 2075,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
"containerimage.digest": "sha256:f0b83dbc066f8e64481d13450dc3fca3ae13708f2d0046b034a2f46d1368242e",
"image.name": "ghcr.io/graphql-hive/usage-ingestor:d06ba598fd8ecc3a7d621a9877229a596f581d55-amd64,ghcr.io/graphql-hive/usage-ingestor:feat_permission_member_roles-amd64"
},
"webhooks": {
"buildx.build.provenance": {
"buildType": "https://mobyproject.org/buildkit@v1",
"materials": [
{
"uri": "pkg:docker/[email protected]?platform=linux%2Famd64",
"digest": {
"sha256": "f5a0871ab03b035c58bdb3007c3d177b001c2145c18e81817b71624dcf7d8bff"
}
}
],
"invocation": {
"configSource": {
"entryPoint": "services.dockerfile"
},
"parameters": {
"frontend": "dockerfile.v0",
"args": {
"build-arg:HEALTHCHECK_CMD": "wget --spider -q http://127.0.0.1:${PORT}/_readiness",
"build-arg:IMAGE_DESCRIPTION": "The webhooks ingestor service of the GraphQL Hive project.",
"build-arg:IMAGE_TITLE": "graphql-hive/webhooks",
"build-arg:PORT": "3005",
"build-arg:RELEASE": "d06ba598fd8ecc3a7d621a9877229a596f581d55",
"build-arg:SERVICE_DIR_NAME": "@hive/webhooks",
"context:dist": "local:dist",
"context:shared": "local:shared",
"frontend.caps": "moby.buildkit.frontend.contexts+forward",
"local-sessionid:context": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:dockerfile": "ojjxc7lh7rxj7w075fb9rgdfc",
"local-sessionid:shared": "ojjxc7lh7rxj7w075fb9rgdfc"
},
"locals": [
{
"name": "context"
},
{
"name": "dist"
},
{
"name": "dockerfile"
},
{
"name": "shared"
}
]
},
"environment": {
"platform": "linux/amd64"
}
}
},
"buildx.build.ref": "builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c6/builder-b4d6e5c0-1bba-4686-9514-082eb4bc16c60/slvrbmhi3nlxad426j0wufi4k",
"containerimage.config.digest": "sha256:ceb80d8ee2dbd0b18fbae0c9e2315b4d45a5d0333334cd12b06c50cde3dcc827",
"containerimage.descriptor": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:fcad81f4f4272945cd4e690a460dd7a5a918bf054b3a32b0df48def01bebb19c",
"size": 2075,
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
"containerimage.digest": "sha256:fcad81f4f4272945cd4e690a460dd7a5a918bf054b3a32b0df48def01bebb19c",
"image.name": "ghcr.io/graphql-hive/webhooks:d06ba598fd8ecc3a7d621a9877229a596f581d55-amd64,ghcr.io/graphql-hive/webhooks:feat_permission_member_roles-amd64"
}
} |
df3bd63
to
d410863
Compare
📚 Storybook DeploymentThe latest changes are available as preview in: https://6b531d36.hive-storybook.pages.dev |
5fdb9f0
to
8fc0739
Compare
a8eadc4
to
d3810a9
Compare
eb82292
to
cb29617
Compare
@coderabbitai summary |
✅ Actions performedSummary regeneration triggered. |
WalkthroughThis update introduces a comprehensive overhaul of the organization member role and permissions system in the Hive platform. The changes replace the existing access scopes with a more granular, permission-based access control mechanism. The new system allows for more precise resource-level permissions across organizations, projects, targets, services, and app deployments. Key modifications include restructuring how member roles are defined, managed, and assigned, with a focus on providing more flexible and detailed access control. Changes
Sequence DiagramsequenceDiagram
participant User
participant Organization
participant MemberRole
participant ResourceAssignment
User->>Organization: Request to assign role
Organization->>MemberRole: Select role
MemberRole->>ResourceAssignment: Define resource access
ResourceAssignment-->>MemberRole: Validate permissions
MemberRole-->>Organization: Confirm role assignment
Organization-->>User: Update member access
Poem
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
🧹 Nitpick comments (28)
packages/services/api/src/modules/auth/lib/authz.ts (3)
330-377
: Consider standardizing permission naming patterns.Some permissions follow different naming patterns:
organization:describe
vsbilling:describe
targetAccessToken:modify
vstarget:modifySettings
Consider standardizing to either:
<resource>:<action>
(e.g.,target:modifyAccessToken
)<feature>:<action>
(e.g.,accessToken:modify
)
426-428
: Enhance error message in getPermissionGroup.The error message could be more helpful by including the available permission groups.
- throw new Error(`Could not find group for permission '${permission}'.`); + throw new Error( + `Could not find group for permission '${permission}'. Available groups: ${Array.from( + new Set(permissionResourceLevelLookupMap.values()) + ).join(', ')}` + );
411-420
: Consider caching the permission lookup map.The
permissionResourceLevelLookupMap
is rebuilt on every application start. Consider:
- Making it a
const
and building it at compile-time- Adding a caching layer for frequently accessed permissions to improve performance
This could be especially beneficial as the number of permissions grows.
packages/services/api/src/modules/organization/providers/organization-member.spec.ts (2)
4-147
: Consider adding error scenario test cases.The current test suite lacks coverage for error scenarios. Consider adding test cases for:
- Invalid project IDs
- Malformed input objects
- Empty or null project arrays in granular mode
Example test case:
test('project granular: should handle invalid project ID', () => { expect(() => resolveResourceAssignment({ organizationId: 'aaa', projects: { mode: 'granular', projects: [ { id: '', // invalid ID targets: { mode: '*' }, type: 'project', }, ], }, }), ).toThrow('Invalid project ID'); });
417-541
: Document the property name transformation for app deployments.There's an undocumented property name transformation for app deployments:
- Input object uses:
appName
(line 434)- Output object uses:
appDeploymentName
(line 469)This transformation should be documented to prevent confusion and ensure maintainability. Consider:
- Adding a comment explaining the transformation
- Creating an interface to define both input and output types
Example:
interface AppDeploymentInput { appName: string; type: 'appDeployment'; } interface AppDeploymentOutput { appDeploymentName: string; // transformed from appName targetId: string; type: 'appDeployment'; }packages/services/api/src/modules/organization/resolvers/Member.ts (1)
28-30
: Remove unnecessary async keyword.The
id
resolver is marked as async but doesn't use any await operations. Consider removing the async keyword for better performance.- id: async (member, _arg, _ctx) => { + id: (member, _arg, _ctx) => { return member.userId; },packages/web/app/src/pages/target-settings.tsx (1)
Line range hint
1313-1400
: Consider enhancing error handling and user protection.The delete functionality works well but could be improved by:
- Adding more specific error messages for different failure cases
- Implementing rate limiting or debouncing for the delete action
Consider applying this diff:
const handleDelete = async () => { + // Prevent rapid consecutive clicks + if (isDeleting) return; + setIsDeleting(true); const { error } = await mutate({ selector: { organizationSlug, projectSlug, targetSlug, }, }); if (error) { + // Provide more specific error messages + const errorMessage = error.graphQLErrors?.[0]?.extensions?.code === 'FORBIDDEN' + ? 'You do not have permission to delete this target' + : error.message; toast({ variant: 'destructive', title: 'Failed to delete target', - description: error.message, + description: errorMessage, }); } else { toast({ title: 'Target deleted', description: 'The target has been successfully deleted.', }); props.toggleModalOpen(); void router.navigate({ to: '/$organizationSlug/$projectSlug', params: { organizationSlug, projectSlug, }, }); } + setIsDeleting(false); };packages/web/app/src/components/organization/members/selected-permission-overview.tsx (2)
41-80
: Consider memoizing the component's return value.The component maps over a static array on each render. While this might not cause performance issues with small arrays, it's a good practice to memoize the result using
useMemo
since the mapping operation depends only on the group level and title.- return [ + return useMemo(() => [ { level: PermissionLevel.Organization, title: 'Organization', }, // ... other items ].map(group => ( <PermissionLevelGroup key={group.level} permissionLevel={group.level} title={group.title} activePermissionIds={activePermissionIds} memberPermissionGroups={organization.availableMemberPermissionGroups} showOnlyAllowedPermissions={props.showOnlyAllowedPermissions} isExpanded={props.isExpanded ?? true} /> - )); + ), [activePermissionIds, organization.availableMemberPermissionGroups, props.showOnlyAllowedPermissions, props.isExpanded]);
97-133
: Consider extracting the filtering logic into a separate function.The filtering logic in the
useMemo
hook is complex and could benefit from being extracted into a separate function for better readability and maintainability.+const filterGroups = ( + groups: AvailableMembershipPermissions, + permissionLevel: PermissionLevel, + activePermissionIds: ReadonlySet<string> +) => { + let totalAllowedCount = 0; + const filteredGroups: Array< + MembershipPermissionGroup & { + totalAllowedCount: number; + } + > = []; + + for (const group of groups) { + let groupTotalAllowedCount = 0; + const filteredPermissions = group.permissions.filter(permission => { + if (permission.level !== permissionLevel) { + return false; + } + if (activePermissionIds.has(permission.id) || permission.isReadOnly) { + totalAllowedCount++; + groupTotalAllowedCount++; + } + return true; + }); + + if (filteredPermissions.length === 0) { + continue; + } + + filteredGroups.push({ + ...group, + permissions: filteredPermissions, + totalAllowedCount: groupTotalAllowedCount, + }); + } + + return [filteredGroups, totalAllowedCount] as const; +}; const [filteredGroups, totalAllowedCount] = useMemo( - () => { - let totalAllowedCount = 0; - // ... rest of the filtering logic - }, + () => filterGroups(props.memberPermissionGroups, props.permissionLevel, props.activePermissionIds), [props.permissionLevel, props.activePermissionIds, props.memberPermissionGroups] );packages/services/api/src/modules/organization/providers/organization-members.ts (1)
522-524
: TODO comment needs to be addressed.There's a TODO comment indicating that service names are not being validated. This should be addressed to ensure data integrity.
Would you like me to help implement service name validation or create an issue to track this task?
packages/web/app/src/components/organization/members/resource-selector.tsx (2)
25-26
: Fix typo in query name.There's a typo in the query name "Targest" which should be "Targets".
-const ResourceSelector_OrganizationProjectTargestQuery = graphql(` +const ResourceSelector_OrganizationProjectTargetsQuery = graphql(`
477-478
: Add loading state for granular access view.The granular access view doesn't show a loading state while fetching data, which could lead to a poor user experience.
<TabsContent value="granular" variant="content"> + {organizationProjectTargets.fetching ? ( + <div className="flex items-center justify-center p-4"> + <span className="loading loading-spinner loading-md"></span> + <span className="ml-2">Loading resources...</span> + </div> + ) : ( {projectState && ( <> <p className="mb-4 text-sm">The permissions are granted on the specified resources.</p> // ... rest of the code </> )} + )} </TabsContent>packages/services/api/src/modules/organization/resolvers/OrganizationInvitation.ts (1)
16-22
: Enhance error handling and type safety.Consider these improvements:
- Make the error message more descriptive (e.g., "Role not found for invitation").
- Add type checking for
roleId
before the database query.role: async (invitation, _arg, { injector }) => { + if (!invitation.roleId) { + throw new Error('Role ID is required for invitation.'); + } const role = await injector.get(OrganizationMemberRoles).findMemberRoleById(invitation.roleId); if (!role) { - throw new Error('Not found.'); + throw new Error(`Role not found for invitation (ID: ${invitation.roleId}).`); } return role; },.changeset/tall-islands-occur.md (2)
12-14
: Improve clarity and grammar in breaking changes.The breaking changes section needs grammatical improvements:
-- Organization members with the default `Viewer` role, will experience downgraded permissions. They will no longer be able to create targets or projects. -- Organization member roles permissions for inviting, removing or assigning roles have been revoked. A organization admin will have to re-apply the permissions to the desired member roles. -- Organization members with permissions for managing invites, removing members, assigning roles or modifying roles are no longer restrained in granting more rights to other users. Please be aware when granting these permissions to a user role. We recommend only assigning these to member roles that are considered "Admin" user roles. +- Organization members with the default `Viewer` role will experience downgraded permissions. They will no longer be able to create targets or projects. +- Organization member role permissions for inviting, removing, or assigning roles have been revoked. An organization admin will have to re-apply the permissions to the desired member roles. +- Organization members with permissions for managing invites, removing members, assigning roles, or modifying roles are no longer restricted in granting additional rights to other users. Exercise caution when granting these permissions. We recommend assigning these only to member roles that are considered "Admin" user roles.🧰 Tools
🪛 LanguageTool
[uncategorized] ~12-~12: Possible missing comma found.
Context: ...owing breaking changes: - Organization members with the defaultViewer
role, will ex...(AI_HYDRA_LEO_MISSING_COMMA)
[misspelling] ~13-~13: Use “An” instead of ‘A’ if the following word starts with a vowel sound, e.g. ‘an article’, ‘an hour’.
Context: ...g or assigning roles have been revoked. A organization admin will have to re-appl...(EN_A_VS_AN)
16-16
: Fix hyphenation in technical term.-A future update will introduce resource based access control (based on project, target, service or app deployments) for organization members. +A future update will introduce resource-based access control (based on project, target, service, or app deployments) for organization members.🧰 Tools
🪛 LanguageTool
[uncategorized] ~16-~16: This expression is usually spelled with a hyphen.
Context: ... roles. A future update will introduce resource based access control (based on project, targe...(BASED_HYPHEN)
packages/services/api/src/modules/organization/resolvers/MemberRole.ts (1)
41-44
: Consider memoizing the flattened permissions array.The current implementation flattens the permissions array on every request. Consider memoizing the result if this resolver is called frequently, as the permissions are unlikely to change during a request lifecycle.
permissions: (role, _arg, _ctx) => { + if (role._flattenedPermissions) { + return role._flattenedPermissions; + } - return Array.from(Object.values(role.permissions)).flatMap((set: Set<Permission>) => - Array.from(set), - ); + role._flattenedPermissions = Array.from(Object.values(role.permissions)).flatMap((set: Set<Permission>) => + Array.from(set), + ); + return role._flattenedPermissions; },packages/web/app/src/components/organization/members/member-role-picker.tsx (2)
196-222
: Consider debouncing the permission change handler.The
onValueChange
handler directly triggers state updates and API calls. For better performance, consider debouncing these operations when users are rapidly changing multiple permissions.+import { useMemo, useRef, useState, useCallback } from 'react'; +import debounce from 'lodash/debounce'; export function MemberRolePicker(props: PermissionSelectorProps) { + const debouncedPermissionChange = useCallback( + debounce((selectedPermissionIds: Set<string>) => { + props.onSelectedPermissionsChange(selectedPermissionIds); + }, 300), + [props.onSelectedPermissionsChange] + ); // ... existing code ... onValueChange={value => { const dependents = dependencyGraph.get(permission.id) ?? []; if (value === 'allow') { - props.onSelectedPermissionsChange( + debouncedPermissionChange( new Set([...props.selectedPermissionIds, permission.id]), ); } else if (value === 'not-selected') { const selectedPermissionIds = new Set(props.selectedPermissionIds); selectedPermissionIds.delete(permission.id); for (const dependent of dependents) { selectedPermissionIds.delete(dependent); } - props.onSelectedPermissionsChange(selectedPermissionIds); + debouncedPermissionChange(selectedPermissionIds); } setFocusedPermission(null); }}
223-230
: Enhance accessibility of the permission selector.Add ARIA labels to improve screen reader support for the permission selection dropdown.
-<SelectTrigger className="w-[150px] shrink-0"> +<SelectTrigger + className="w-[150px] shrink-0" + aria-label={`Permission level for ${permission.title}`} +> <SelectValue /> </SelectTrigger> <SelectContent> - <SelectItem value="not-selected">Not Selected</SelectItem> - <SelectItem value="allow">Allow</SelectItem> + <SelectItem value="not-selected" aria-label="No access">Not Selected</SelectItem> + <SelectItem value="allow" aria-label="Full access">Allow</SelectItem> </SelectContent>integration-tests/tests/api/project/crud.spec.ts (1)
203-227
: Add edge cases to permission restriction test.The test could be enhanced to cover additional scenarios:
- Attempting to access a restricted project
- Verifying that other members' access is unaffected
test.concurrent('prevent access to projects with assigned resources on member', async () => { const { createOrg } = await initSeed().createOwner(); - const { createProject, inviteAndJoinMember, projects: getProjects } = await createOrg(); + const { createProject, inviteAndJoinMember, projects: getProjects, inviteAndJoinOwner } = await createOrg(); const { project } = await createProject(ProjectType.Single); + const { project: project2 } = await createProject(ProjectType.Single); // By default the viewer will have the "Viewer" role. const { member, assignMemberRole, memberToken } = await inviteAndJoinMember(); + const { memberToken: ownerToken } = await inviteAndJoinOwner(); // By default the user should have access to all the projects within the organization. let projects = await getProjects(memberToken); expect(projects).toHaveLength(1); expect(projects.at(0)?.id).toEqual(project.id); // Limit the users access to no projects using the "Viewer" role await assignMemberRole({ roleId: member.role.id, userId: member.user.id, resouces: { mode: ResourceAssignmentMode.Granular, projects: [], }, }); projects = await getProjects(memberToken); expect(projects).toHaveLength(0); + + // Verify that attempting to access a restricted project fails + const projectResult = await getProject(memberToken, project.id); + expect(projectResult.error).toBeDefined(); + + // Verify that other members' access is unaffected + const ownerProjects = await getProjects(ownerToken); + expect(ownerProjects).toHaveLength(2); });packages/services/api/src/modules/organization/lib/organization-member-permissions.ts (2)
3-10
: Consider adding validation for permission IDs.The
PermissionRecord
type could benefit from runtime validation to ensure permission IDs follow a consistent format.+const PERMISSION_ID_PATTERN = /^[a-z]+:[a-z]+$/; + +function validatePermissionId(id: string): boolean { + return PERMISSION_ID_PATTERN.test(id); +} + export type PermissionRecord = { id: Permission; title: string; description: string; dependsOn?: Permission; isReadyOnly?: true; warning?: string; };
249-267
: Enhance error reporting in permission validation.The error message could be more informative by including details about why each permission is unassigned.
function assertAllRulesAreAssigned(excluded: Array<Permission>) { const permissionsToCheck = new Set(allPermissions); + const unassignedReasons = new Map<Permission, string>(); for (const item of excluded) { permissionsToCheck.delete(item); } for (const group of allPermissionGroups) { for (const permission of group.permissions) { permissionsToCheck.delete(permission.id); } } if (permissionsToCheck.size) { + const details = Array.from(permissionsToCheck).map(permission => { + const reason = unassignedReasons.get(permission) || 'Not found in any permission group'; + return `${permission}: ${reason}`; + }); throw new Error( - 'The following permissions are not assigned: \n' + Array.from(permissionsToCheck).join(`\n`), + 'The following permissions are not assigned: \n' + details.join('\n'), ); } }packages/web/app/src/components/organization/members/permission-selector.tsx (2)
125-128
: Fix arbitrary class name warning.The arbitrary class name
-m-[1px]
should be replaced with a positive margin and transform.-'flex flex-row items-center justify-between space-x-4 border-orange-500 pb-2 text-sm', -focusedPermission === permission.id && '-m-[1px] border', +'flex flex-row items-center justify-between space-x-4 border-orange-500 pb-2 text-sm', +focusedPermission === permission.id && 'border translate-x-[1px] translate-y-[1px]',🧰 Tools
🪛 GitHub Check: code-style / eslint-and-prettier
[warning] 125-125:
Arbitrary value classname '-m-[1px]' should not start with a dash (-)
93-95
: Consider persisting accordion state.The open/closed state of accordions is reset when the component re-renders. Consider persisting this state in localStorage for a better user experience.
-const [openAccordions, setOpenAccordions] = useState([] as Array<string>); +const [openAccordions, setOpenAccordions] = useState(() => { + try { + const saved = localStorage.getItem('permission-selector-accordions'); + return saved ? JSON.parse(saved) : []; + } catch { + return []; + } +}); + +useEffect(() => { + localStorage.setItem('permission-selector-accordions', JSON.stringify(openAccordions)); +}, [openAccordions]);packages/services/api/src/modules/organization/module.graphql.ts (1)
470-483
: Consider adding validation for resource names.The
AppDeploymentResourceAssignmentInput
andServiceResourceAssignmentInput
accept string inputs for names. Consider adding:
- Pattern validation for allowed characters
- Length restrictions
- Documentation about naming conventions
Example addition:
""" Name must be alphanumeric with hyphens, underscores, and dots. Maximum length is 64 characters. """ appDeployment: String! @pattern(regexp: "^[a-zA-Z0-9-_\\.]{1,64}$")Also applies to: 484-498
packages/services/api/src/modules/support/providers/support-manager.ts (1)
199-208
: Improve error handling for missing user data.The error message "Missing user." is too generic. Consider providing more context about which user is missing and why.
- throw new Error('Missing user.'); + throw new Error(`User with ID ${membership.userId} not found in storage.`);packages/services/api/src/modules/auth/lib/supertokens-strategy.ts (1)
329-330
: Consider adding an error message that lists the unhandled case.The
casesExhausted
function could provide more context about which case was not handled.-function casesExhausted(_value: never): never { - throw new Error('Not all cases were handled.'); +function casesExhausted(value: never): never { + throw new Error(`Not all cases were handled. Unhandled case: ${JSON.stringify(value)}`); }packages/services/api/src/modules/organization/providers/organization-member-roles.ts (1)
223-310
: Consider adding documentation for the legacy scope transformation.The
transformOrganizationMemberLegacyScopesIntoPermissionGroup
function handles complex legacy scope transformation but lacks documentation explaining the mapping rationale.Add JSDoc comments explaining:
- The purpose of the transformation
- The mapping between legacy scopes and new permissions
- Any special cases or considerations
+/** + * Transforms legacy organization member scopes into the new permission-based system. + * + * @param scopes - Array of legacy scopes to transform + * @returns Object containing mapped permissions per resource level + * + * Special cases: + * - Member:modify permission is not assigned from MEMBERS scope for safety + * - Default permissions are always included + */ function transformOrganizationMemberLegacyScopesIntoPermissionGroup(packages/web/app/src/components/organization/members/roles.tsx (1)
132-139
: Consider using a callback ref for form value updates.The permission selection logic could be optimized to avoid potential race conditions.
- const onChangeSelectedPermissions = useCallback((permissions: ReadonlySet<string>) => { - setSelectedPermissions(new Set(permissions)); - form.setValue('selectedPermissions', [...permissions]); - }, []); + const onChangeSelectedPermissions = useCallback((permissions: ReadonlySet<string>) => { + setSelectedPermissions(new Set(permissions)); + form.setValue('selectedPermissions', [...permissions], { + shouldValidate: true, + shouldDirty: true, + }); + }, [form]);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (75)
.changeset/tall-islands-occur.md
(1 hunks)integration-tests/testkit/flow.ts
(6 hunks)integration-tests/testkit/seed.ts
(8 hunks)integration-tests/tests/api/organization/members.spec.ts
(1 hunks)integration-tests/tests/api/organization/transfer.spec.ts
(2 hunks)integration-tests/tests/api/project/crud.spec.ts
(2 hunks)integration-tests/tests/api/schema/check.spec.ts
(2 hunks)integration-tests/tests/api/target/crud.spec.ts
(1 hunks)packages/migrations/src/actions/2025.01.30T00-00-00.granular-member-role-permissions.ts
(1 hunks)packages/migrations/src/run-pg-migrations.ts
(1 hunks)packages/services/api/src/index.ts
(1 hunks)packages/services/api/src/modules/app-deployments/providers/app-deployments-manager.ts
(0 hunks)packages/services/api/src/modules/app-deployments/providers/app-deployments.ts
(1 hunks)packages/services/api/src/modules/app-deployments/resolvers/Target.ts
(2 hunks)packages/services/api/src/modules/auth/lib/authz.ts
(3 hunks)packages/services/api/src/modules/auth/lib/supertokens-strategy.ts
(6 hunks)packages/services/api/src/modules/auth/lib/target-access-token-strategy.ts
(0 hunks)packages/services/api/src/modules/auth/module.graphql.mappers.ts
(2 hunks)packages/services/api/src/modules/auth/module.graphql.ts
(1 hunks)packages/services/api/src/modules/auth/providers/auth-manager.ts
(0 hunks)packages/services/api/src/modules/auth/resolvers/Member.ts
(0 hunks)packages/services/api/src/modules/auth/resolvers/Permission.ts
(1 hunks)packages/services/api/src/modules/auth/resolvers/PermissionGroup.ts
(1 hunks)packages/services/api/src/modules/oidc-integrations/providers/oidc-integrations.provider.ts
(1 hunks)packages/services/api/src/modules/oidc-integrations/resolvers/OIDCIntegration.ts
(1 hunks)packages/services/api/src/modules/organization/index.ts
(2 hunks)packages/services/api/src/modules/organization/lib/organization-member-permissions.ts
(1 hunks)packages/services/api/src/modules/organization/module.graphql.mappers.ts
(1 hunks)packages/services/api/src/modules/organization/module.graphql.ts
(5 hunks)packages/services/api/src/modules/organization/providers/organization-config.ts
(0 hunks)packages/services/api/src/modules/organization/providers/organization-manager.ts
(22 hunks)packages/services/api/src/modules/organization/providers/organization-member-roles.ts
(1 hunks)packages/services/api/src/modules/organization/providers/organization-member.spec.ts
(1 hunks)packages/services/api/src/modules/organization/providers/organization-members.ts
(1 hunks)packages/services/api/src/modules/organization/resolvers/Member.ts
(1 hunks)packages/services/api/src/modules/organization/resolvers/MemberConnection.ts
(1 hunks)packages/services/api/src/modules/organization/resolvers/MemberRole.ts
(1 hunks)packages/services/api/src/modules/organization/resolvers/Mutation/assignMemberRole.ts
(1 hunks)packages/services/api/src/modules/organization/resolvers/Mutation/createMemberRole.ts
(1 hunks)packages/services/api/src/modules/organization/resolvers/Mutation/updateMemberRole.ts
(1 hunks)packages/services/api/src/modules/organization/resolvers/Organization.ts
(7 hunks)packages/services/api/src/modules/organization/resolvers/OrganizationInvitation.ts
(2 hunks)packages/services/api/src/modules/schema/providers/schema-version-helper.ts
(1 hunks)packages/services/api/src/modules/shared/providers/storage.ts
(2 hunks)packages/services/api/src/modules/support/providers/support-manager.ts
(5 hunks)packages/services/api/src/modules/token/providers/token-manager.ts
(3 hunks)packages/services/api/src/shared/entities.ts
(2 hunks)packages/services/api/src/shared/helpers.ts
(1 hunks)packages/services/server/src/index.ts
(2 hunks)packages/services/storage/src/db/types.ts
(2 hunks)packages/services/storage/src/index.ts
(24 hunks)packages/web/app/src/components/layouts/organization.tsx
(0 hunks)packages/web/app/src/components/organization/Permissions.tsx
(0 hunks)packages/web/app/src/components/organization/members/common.tsx
(2 hunks)packages/web/app/src/components/organization/members/list.tsx
(5 hunks)packages/web/app/src/components/organization/members/member-role-picker.tsx
(1 hunks)packages/web/app/src/components/organization/members/member-role-selector.tsx
(1 hunks)packages/web/app/src/components/organization/members/permission-selector.tsx
(1 hunks)packages/web/app/src/components/organization/members/resource-selector.tsx
(1 hunks)packages/web/app/src/components/organization/members/roles.tsx
(16 hunks)packages/web/app/src/components/organization/members/selected-permission-overview.tsx
(1 hunks)packages/web/app/src/components/target/settings/registry-access-token.tsx
(1 hunks)packages/web/app/src/lib/access/common.ts
(1 hunks)packages/web/app/src/lib/access/organization.ts
(0 hunks)packages/web/app/src/lib/access/project.ts
(0 hunks)packages/web/app/src/lib/access/target.ts
(0 hunks)packages/web/app/src/lib/hooks/laboratory/use-operation-collections-plugin.tsx
(0 hunks)packages/web/app/src/pages/organization-members.tsx
(1 hunks)packages/web/app/src/pages/project-policy.tsx
(0 hunks)packages/web/app/src/pages/project-settings.tsx
(0 hunks)packages/web/app/src/pages/target-settings.tsx
(4 hunks)packages/web/app/src/pages/target.tsx
(1 hunks)packages/web/app/src/stories/permission-selector.stories.tsx
(1 hunks)packages/web/app/src/stories/selected-permission-overview.stories.tsx
(1 hunks)packages/web/app/src/stories/utils.ts
(1 hunks)
💤 Files with no reviewable changes (13)
- packages/services/api/src/modules/auth/lib/target-access-token-strategy.ts
- packages/services/api/src/modules/auth/resolvers/Member.ts
- packages/web/app/src/components/layouts/organization.tsx
- packages/services/api/src/modules/app-deployments/providers/app-deployments-manager.ts
- packages/web/app/src/lib/access/target.ts
- packages/web/app/src/pages/project-settings.tsx
- packages/services/api/src/modules/organization/providers/organization-config.ts
- packages/web/app/src/lib/hooks/laboratory/use-operation-collections-plugin.tsx
- packages/web/app/src/pages/project-policy.tsx
- packages/web/app/src/lib/access/organization.ts
- packages/web/app/src/components/organization/Permissions.tsx
- packages/services/api/src/modules/auth/providers/auth-manager.ts
- packages/web/app/src/lib/access/project.ts
🚧 Files skipped from review as they are similar to previous changes (26)
- packages/services/api/src/modules/auth/resolvers/PermissionGroup.ts
- packages/services/api/src/modules/organization/resolvers/Mutation/assignMemberRole.ts
- packages/web/app/src/components/target/settings/registry-access-token.tsx
- packages/migrations/src/actions/2025.01.30T00-00-00.granular-member-role-permissions.ts
- packages/services/api/src/modules/organization/index.ts
- packages/services/api/src/modules/oidc-integrations/resolvers/OIDCIntegration.ts
- packages/migrations/src/run-pg-migrations.ts
- integration-tests/tests/api/target/crud.spec.ts
- packages/services/api/src/index.ts
- packages/services/api/src/modules/app-deployments/providers/app-deployments.ts
- packages/services/server/src/index.ts
- packages/services/api/src/modules/organization/resolvers/MemberConnection.ts
- integration-tests/tests/api/organization/transfer.spec.ts
- packages/services/api/src/modules/auth/resolvers/Permission.ts
- packages/web/app/src/stories/utils.ts
- packages/services/api/src/modules/organization/resolvers/Mutation/updateMemberRole.ts
- packages/services/api/src/shared/helpers.ts
- packages/web/app/src/stories/selected-permission-overview.stories.tsx
- packages/web/app/src/pages/organization-members.tsx
- packages/services/api/src/modules/oidc-integrations/providers/oidc-integrations.provider.ts
- packages/web/app/src/components/organization/members/member-role-selector.tsx
- packages/web/app/src/stories/permission-selector.stories.tsx
- packages/services/storage/src/db/types.ts
- packages/services/api/src/modules/schema/providers/schema-version-helper.ts
- packages/services/api/src/modules/auth/module.graphql.mappers.ts
- packages/web/app/src/components/organization/members/common.tsx
👮 Files not reviewed due to content moderation or server errors (3)
- packages/services/api/src/modules/organization/providers/organization-manager.ts
- integration-tests/testkit/seed.ts
- integration-tests/tests/api/schema/check.spec.ts
🧰 Additional context used
🪛 GitHub Check: code-style / eslint-and-prettier
packages/web/app/src/components/organization/members/permission-selector.tsx
[warning] 125-125:
Arbitrary value classname '-m-[1px]' should not start with a dash (-)
🪛 LanguageTool
.changeset/tall-islands-occur.md
[uncategorized] ~12-~12: Possible missing comma found.
Context: ...owing breaking changes: - Organization members with the default Viewer
role, will ex...
(AI_HYDRA_LEO_MISSING_COMMA)
[misspelling] ~13-~13: Use “An” instead of ‘A’ if the following word starts with a vowel sound, e.g. ‘an article’, ‘an hour’.
Context: ...g or assigning roles have been revoked. A organization admin will have to re-appl...
(EN_A_VS_AN)
[uncategorized] ~16-~16: This expression is usually spelled with a hyphen.
Context: ... roles. A future update will introduce resource based access control (based on project, targe...
(BASED_HYPHEN)
🔇 Additional comments (61)
packages/services/api/src/modules/auth/lib/authz.ts (4)
144-148
: Well-placed debug logging statements!The added logging will help in debugging permission-related issues by providing visibility into resolved permissions and resource IDs.
330-377
: Excellent permission structure with clear resource-level separation!The new permission system is well-organized by resource levels and uses Zod for robust type validation.
379-453
: Efficient and type-safe permission management implementation!The utility functions and types provide a robust foundation for permission handling with:
- Efficient Map-based lookups
- Strong type safety through Zod
- Clear transformation functions
459-469
: Smart use of dynamic generation for action definitions!The implementation:
- Reduces code duplication
- Maintains type safety with
satisfies
- Makes permission-to-action mapping maintenance easier
packages/services/api/src/modules/organization/providers/organization-member.spec.ts (5)
1-3
: LGTM! Test suite setup follows best practices.The import statement and test suite structure are well-organized.
4-147
: LGTM! Project-level permission tests are comprehensive.The test cases effectively cover wildcard access, single project access, and multiple projects access scenarios. The test data structure and assertions are consistent and well-organized.
148-277
: LGTM! Target-level permission tests are well-structured.The test cases effectively validate single and multiple target access scenarios. The test data structure maintains consistency with project-level tests and properly sets up related resources (services and app deployments).
278-416
: Verify property name consistency in service objects.In the service objects, there's an inconsistency in property naming:
- Input object uses:
serviceName
(line 298)- Output object uses:
serviceName
(line 330)While they currently match, it's worth documenting this as an explicit contract since these property names are crucial for the permission system. Consider adding a type definition or interface to enforce this contract.
1-541
: LGTM! Comprehensive test coverage for the permission system.The test suite provides thorough coverage of the new granular permission system, effectively testing:
- Organization-wide access
- Project-level permissions
- Target-level permissions
- Service-level permissions
- App deployment-level permissions
Each level properly validates both single and multiple resource scenarios, ensuring the hierarchical permission structure works as intended.
packages/services/api/src/modules/organization/resolvers/Member.ts (6)
1-6
: LGTM! Import changes and type declaration updates align with PR objectives.The transition from
Pick<MemberResolvers>
toMemberResolvers
indicates a complete implementation of all resolver methods, which aligns with the granular permission system overhaul.
7-10
: LGTM! Simplified canLeaveOrganization implementation.The simplified implementation improves code maintainability while maintaining the same functionality.
25-27
: LGTM! Clear and focused role resolver implementation.The implementation directly returns the role from assignedRole, maintaining simplicity.
31-37
: LGTM! Robust user resolver with proper error handling.The implementation includes appropriate error handling for missing users and correctly uses dependency injection.
38-40
: LGTM! Well-structured resourceAssignment resolver.The implementation follows separation of concerns by delegating to the specialized OrganizationMembers provider.
12-24
: Verify the permission action change impact.The action has been changed from 'member:removeMember' to 'member:modify'. Please ensure this aligns with the new permission system and all consumers are updated accordingly.
✅ Verification successful
Permission action change is properly implemented
The migration from 'member:removeMember' to 'member:modify' is complete and consistent across all relevant parts of the codebase, including backend services, frontend components, and tests.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Search for any remaining occurrences of the old permission action rg "member:removeMember" --type ts # Search for all usages of the new permission action to verify consistency rg "member:modify" --type tsLength of output: 2351
packages/web/app/src/pages/target.tsx (1)
334-335
: LGTM! Verify access control migration.The simplified conditional rendering aligns with the new permission system. The removal of organization-level checks is appropriate as access control is now handled through granular permissions.
Let's verify that the access control is properly handled by the new permission system:
✅ Verification successful
✅ Access control migration verified
The removal of organization-level checks is safe as access control is now properly handled through the new granular permission system at the API level. The project and target-level checks are sufficient for rendering control.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Check for any remaining organization-based access checks in the codebase # that might have been missed during the migration. # Look for organization-related access control patterns rg -g '*.{ts,tsx}' -i '(organization.*access|organization.*permission|organization.*scope)' # Look for potentially missed organization fragments in GraphQL queries ast-grep --pattern 'fragment $_OrganizationFragment on Organization { $$$ }'Length of output: 28228
packages/web/app/src/pages/target-settings.tsx (2)
Line range hint
1280-1313
: LGTM! Improved null safety handling.The changes enhance type safety by:
- Adding explicit null checks before rendering
- Using type narrowing with the conditional check
- Preventing potential runtime errors with early returns
This is a good improvement to the codebase.
139-153
: Verify permission checks for registry token management.With the removal of the
canManage
check andRegistryAccessTokens_MeFragment
, ensure that permissions for creating and deleting registry tokens are properly enforced at a higher level (e.g., route level or parent component).✅ Verification successful
Permission checks for registry tokens are properly implemented at higher levels
The removal of the local
canManage
check is safe as the component is protected by theviewerCanModifyTargetAccessToken
permission check at both the route and API levels. The component is only rendered when the user has the required permissions.🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Verify permission checks for registry token management. # Search for permission checks related to registry tokens rg -A 5 "viewerCanModifyTargetAccessToken|canManage.*registry.*token" --type ts --type tsx # Search for route-level permission checks rg -A 5 "useRedirect.*registry.*token" --type ts --type tsxLength of output: 210
Script:
#!/bin/bash # Search for permission-related code and GraphQL operations rg -t typescript -t javascriptreact -t typescriptreact -A 5 "canManage|viewerCan|usePermission|isAuthorized" # Look for GraphQL operations related to registry tokens ast-grep --pattern 'mutation $_ { $$$ registryToken $$$ }' # Search for route protection or higher-level permission checks rg -t typescript -t javascriptreact -t typescriptreact -A 5 "useRedirect|ProtectedRoute|RequirePermission"Length of output: 356
Script:
#!/bin/bash # Look for the target-settings file and its content fd "target-settings.tsx" --exec cat {} # Search for permission-related patterns without file type restrictions rg "ProtectedRoute|RequirePermission|canManage|viewerCan" # Look for GraphQL operations related to registry tokens rg "mutation.*createRegistryToken|mutation.*deleteRegistryToken" -A 10Length of output: 66469
packages/web/app/src/components/organization/members/selected-permission-overview.tsx (2)
1-13
: LGTM! Clean and well-organized imports.The imports are properly organized and use named imports where appropriate. The component follows React best practices by importing necessary hooks and utilities.
174-196
: 🛠️ Refactor suggestionAdd error handling for permission status updates.
The permission status update logic lacks error handling. Consider adding error boundaries or try-catch blocks to handle potential failures gracefully.
{props.activePermissionIds.has(permission.id) || permission.isReadOnly ? ( permission.warning ? ( <TooltipProvider> <Tooltip> - <TooltipTrigger> + <TooltipTrigger onError={(e) => { + console.error('Failed to render tooltip trigger:', e); + // Fallback to a simpler UI + return ( + <span className="text-yellow-700"> + <CheckIcon className="inline size-4" /> Allowed (Warning) + </span> + ); + }}> <span className="text-yellow-700"> <CheckIcon className="inline size-4" /> Allowed </span> </TooltipTrigger> <TooltipContent>{permission.warning}</TooltipContent> </Tooltip> </TooltipProvider> ) : ( // ... rest of the code ) )}Likely invalid or redundant comment.
packages/services/api/src/modules/organization/resolvers/Mutation/createMemberRole.ts (1)
9-14
: Verify input validation in OrganizationManager.The validation layer has been removed from this resolver. Ensure that proper validation of
selectedPermissions
is implemented inOrganizationManager.createMemberRole
to prevent invalid permission strings.packages/services/api/src/modules/organization/module.graphql.mappers.ts (1)
6-7
: LGTM! Good modularization of member-related types.The separation of member-related types and imports improves code organization and maintainability.
Also applies to: 14-15
packages/web/app/src/lib/access/common.ts (1)
2-2
: Verify all usages of removed access scopes.The removal of organization and project access scopes is a breaking change. Ensure all existing code has been updated to use the new permission system.
packages/services/api/src/modules/organization/resolvers/MemberRole.ts (1)
6-15
: LGTM! Consistent permission check pattern.The refactored permission checks follow a consistent pattern across all methods, making the code more maintainable and easier to understand.
Also applies to: 18-27, 30-36
packages/services/api/src/modules/app-deployments/resolvers/Target.ts (1)
Line range hint
32-43
: Verify authorization handling after session check removal.The session-based authorization check has been removed, and the function now returns
true
by default after the feature flag check. This could potentially expose app deployment information to unauthorized users if authorization is not properly handled at a different layer.Run the following script to verify authorization handling:
✅ Verification successful
Authorization is properly handled through manager-level checks
The removal of the session check from
viewerCanViewAppDeployments
is appropriate as this function only controls feature visibility. Critical operations (create, publish, retire) are properly secured throughassertPerformAction
checks in the app-deployments-manager:
appDeployment:create
appDeployment:publish
appDeployment:retire
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Check if app deployment access is protected by other authorization mechanisms # Look for authorization checks in the app deployment module rg -A 5 "canPerformAction|assertPerformAction" "packages/services/api/src/modules/app-deployments" # Check if there are any GraphQL guards or middleware ast-grep --pattern 'class $_ implements $_ { $$$ @$_("auth") $$$ }'Length of output: 3157
Script:
#!/bin/bash # Check Target resolver and its fields for auth decorators ast-grep --pattern 'class Target { $$$ }' # Check for auth decorators at field level ast-grep --pattern '@$_("auth") $_' # Look for parent resolvers that might include auth checks rg -l "class.*implements.*Resolver" packages/services/api/src/modules/app-deployments/resolvers/Length of output: 182
Script:
#!/bin/bash # Search for resolver class with more flexible pattern ast-grep --pattern 'class $_ { viewerCanViewAppDeployments($$$) { $$$ } }' # Check for module definitions and guards rg -A 5 "@Module|@UseGuards" packages/services/api/src/modules/app-deployments/ # Look for any GraphQL decorators rg -A 2 "@.*Field|@.*Resolver" packages/services/api/src/modules/app-deployments/resolvers/Length of output: 260
Script:
#!/bin/bash # Check for global guards and auth setup rg -A 5 "GlobalGuard|AuthGuard|SessionGuard" packages/services/api/src/ # Look for session initialization and middleware rg -A 5 "implements NestMiddleware|SessionMiddleware" packages/services/api/src/ # Check main.ts or app.module for global auth setup fd -e ts "main.ts|app.module.ts" packages/services/api/src/ -x cat {}Length of output: 230
packages/services/api/src/modules/auth/module.graphql.ts (2)
100-106
: LGTM! Well-structured permission levels.The
PermissionLevel
enum provides a clear hierarchy of permission scopes from organization down to app deployment level.
108-116
: Verify impact of schema changes on existing clients.The introduction of the new permission system with
Permission
type represents a breaking change. Ensure that:
- All clients are updated to handle the new permission structure
- A migration path is provided for clients using the old Member types
Run the following script to identify potentially affected queries:
packages/services/api/src/modules/token/providers/token-manager.ts (1)
39-39
: LGTM! Clean dependency injection.The addition of
OrganizationMembers
follows the dependency injection pattern consistently.integration-tests/tests/api/organization/members.spec.ts (2)
8-39
: LGTM! Comprehensive permission snapshot testing.The snapshot test effectively captures the complete set of permissions for an organization owner, making it easy to track permission changes.
47-54
: Verify test coverage for all permission combinations.While the basic permission tests are good, consider adding tests for:
- Permission inheritance and dependencies
- Permission conflicts resolution
- Edge cases in permission assignment
Run the following script to analyze test coverage:
Also applies to: 64-69
packages/services/api/src/modules/organization/resolvers/Organization.ts (4)
35-41
: Improved error handling in owner resolver.The addition of explicit error handling for the case when an owner is not found enhances the robustness of the resolver.
46-55
: Improved error handling in me resolver.The addition of explicit error handling for the case when a member is not found enhances the robustness of the resolver.
185-187
: LGTM: New resolver for permission groups.The new resolver
availableMemberPermissionGroups
correctly exposes the permission groups through the GraphQL API.
151-151
: Verify the impact of consolidating permission actions.The change from specific actions to the generic
member:modify
action for multiple resolvers (viewerCanManageInvitations
,viewerCanAssignUserRoles
,viewerCanManageRoles
) might impact granular access control.Also applies to: 160-160, 169-169
✅ Verification successful
Permission consolidation is properly implemented and documented
The consolidation to
member:modify
is complete and well-structured:
- Properly defined with clear scope: "Member can invite users, update and assign roles"
- Includes security considerations with explicit warning about permission elevation
- Maintains proper access control by requiring
member:describe
permission🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Check for any other occurrences of the specific actions that were consolidated # to ensure complete migration and no leftover references echo "Searching for old permission actions..." rg -l "member:(assignRole|manageInvitation|manageRole)" --type ts echo "Searching for new consolidated action..." rg -l "member:modify" --type tsLength of output: 1052
Script:
#!/bin/bash # Check permission definitions and migration documentation echo "=== Checking permission definitions ===" rg -A 5 -B 5 "member:modify" packages/services/api/src/modules/organization/lib/organization-member-permissions.ts echo -e "\n=== Checking authorization logic ===" rg -A 5 -B 5 "member:modify" packages/services/api/src/modules/auth/lib/authz.ts echo -e "\n=== Looking for migration documentation ===" fd -e md -e txt | xargs rg -l "member:modify"Length of output: 1285
packages/services/api/src/modules/organization/module.graphql.ts (3)
243-246
: LGTM! Good addition of permission groups.The
availableMemberPermissionGroups
field provides a clear way to expose assignable permissions to the UI, which is essential for the new granular permission system.
447-458
: LGTM! Well-structured Member type.The Member type is well-designed with:
- Clear ownership indication
- Resource assignment integration
- Proper permission checks for member management
522-531
: LGTM! Comprehensive resource assignment structure.The
ResourceAssignmentInput
type provides a flexible way to:
- Assign permissions at organization level
- Control access to specific projects
- Support both granular and broad access patterns
packages/services/api/src/modules/support/providers/support-manager.ts (1)
Line range hint
315-337
: Verify Zendesk organization connection logic.The organization connection logic uses string-to-number conversion which could potentially fail. Consider adding validation for the parsed integers.
packages/services/api/src/modules/shared/providers/storage.ts (1)
208-209
: LGTM! Well-structured method signatures.The new methods
findProjectsByIds
andfindTargetsByIds
are well-defined and follow the interface's existing patterns. The use ofMap
as the return type is a good choice for efficient lookups.Also applies to: 307-311
integration-tests/testkit/flow.ts (1)
53-56
: LGTM! Clean transition to permission-based system.The changes successfully replace the legacy access scopes with the new permissions structure in GraphQL queries and mutations.
Also applies to: 182-186, 219-219
packages/services/storage/src/index.ts (9)
22-22
: LGTM! Added Target type import.The addition of the
Target
type to the imports is consistent with the new target-related functionality being added.
230-230
: LGTM! Simplified role ID assignment.The change to directly use
invitation.role.id
improves code readability and reduces unnecessary object nesting.
Line range hint
509-515
: LGTM! Improved SQL query readability.The SQL query for OIDC integration member role assignment has been reformatted for better readability while maintaining the same functionality.
1401-1430
: LGTM! Well-implemented batch function for project lookups.The
findProjectsByIds
batch function is well-implemented with:
- Proper type safety
- Efficient batch processing
- Clear error handling
- Map-based result caching
1722-1765
: LGTM! Well-structured batch function for target lookups.The
findTargetsByIds
batch function follows the same pattern asfindProjectsByIds
and includes:
- Organization-based batching
- Efficient SQL query
- Proper result mapping
Line range hint
2267-2294
: LGTM! Comprehensive schema version query.The schema version query is well-structured with:
- All necessary fields selected
- Proper table joins
- Clear filtering conditions
5144-5144
: LGTM! Added Zendesk user ID field.The addition of the Zendesk user ID field in the user model is consistent with the Zendesk integration functionality.
528-539
: Optimize database query to fetch only required field.The query has been simplified to fetch only the
id
field, which is more efficient. However, consider adding an index on(organization_id, name)
if not already present to optimize the WHERE clause.Let's verify if the index exists:
Line range hint
1925-1950
: Verify date range query performance.The schema version count query uses date range filtering. Consider adding an index on
created_at
if not already present to optimize these range queries.Let's check for the index:
✅ Verification successful
Existing indices already optimize the date range queries
The codebase already has composite indices on the
schema_versions
table that include thecreated_at
column:
schema_versions_cursor_pagination
(target_id, created_at DESC, id)schema_versions_cursor_pagination_composable
(same columns)These indices will efficiently support the date range filtering in your queries. No additional index is needed.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Check if index exists on schema_versions(created_at) rg -A 5 'CREATE.*INDEX.*schema_versions.*created_at'Length of output: 54
Script:
#!/bin/bash # Look for schema_versions table definition and any related migrations echo "=== Searching for table creation ===" rg -i "create.*table.*schema_versions" -A 10 echo -e "\n=== Searching for migrations mentioning schema_versions ===" fd -e sql -e ts -e js . | xargs rg -l "schema_versions" echo -e "\n=== Searching for any index on schema_versions ===" rg -i "create.*index.*schema_versions"Length of output: 2353
Script:
#!/bin/bash # Check the full definition of cursor pagination indices rg -A 5 '"schema_versions_cursor_pagination.*" ON "schema_versions"'Length of output: 1620
packages/services/api/src/shared/entities.ts (2)
195-196
: LGTM! The addition ofownerId
improves ownership tracking.The new
ownerId
property in theOrganization
interface explicitly tracks organization ownership, which aligns with the granular permission system being introduced.
205-205
: LGTM! The change fromrole
toroleId
improves data modeling.Replacing
role: OrganizationMemberRole
withroleId: string
in theOrganizationInvitation
interface indicates a move towards better referential integrity by using IDs instead of embedding the entire role object.packages/services/api/src/modules/auth/lib/supertokens-strategy.ts (4)
8-12
: LGTM! The new imports support granular permissions.The added imports from the organization members module provide the necessary types and functionality for the new permission system.
37-41
: Enhanced debugging through detailed logging.The added debug logs improve observability by providing clear context about:
- Policy statement loading
- Organization ID validation
- Membership resolution
- Role translation
Also applies to: 44-48, 53-57, 65-69, 76-80, 91-95
121-155
: LGTM! Type-safe resource identification.The
toResourceIdentifier
method provides a type-safe way to generate resource identifiers for different resource types. The method uses function overloads to ensure type safety and handles all possible resource types.
157-210
: LGTM! Granular permission translation.The
translateAssignedRolesToAuthorizationPolicyStatements
method effectively translates role assignments into authorization policy statements, handling different resource types and their associated permissions.packages/services/api/src/modules/organization/providers/organization-member-roles.ts (3)
26-66
: LGTM! Well-structured role model with validation.The
MemberRoleModel
effectively:
- Validates role properties using zod
- Handles predefined roles (Viewer, Admin)
- Transforms legacy scopes into permissions
159-190
: LGTM! Safe role creation with permission filtering.The
createOrganizationMemberRole
method:
- Filters permissions to only include assignable ones
- Uses SQL parameters to prevent injection
- Returns the created role with proper validation
327-344
: LGTM! Clear predefined role permissions.The
predefinedRolesPermissions
object clearly defines:
- Admin role with full access
- Viewer role with read-only permissions
packages/web/app/src/components/organization/members/list.tsx (2)
33-53
: LGTM! Type-safe auth provider mapping.The updated
authProviderToIconAndTextMap
uses the GraphQL schema types directly, improving type safety.
244-279
: LGTM! Clear and user-friendly role display.The
MemberRole
component effectively displays:
- Current role name
- Resource assignment information
- Change role option when permitted
packages/web/app/src/components/organization/members/roles.tsx (1)
73-73
: LGTM! Simplified role form schema.The schema now uses a single
selectedPermissions
array instead of separate scope arrays, aligning with the new permission system.
packages/services/api/src/modules/organization/providers/organization-members.ts
Show resolved
Hide resolved
async findOrganizationMembershipByEmail( | ||
organization: Organization, | ||
email: string, | ||
): Promise<OrganizationMembership | null> { | ||
this.logger.debug( | ||
'Find organization membership by email. (organizationId=%s, email=%s)', | ||
organization.id, | ||
email, | ||
); | ||
const query = sql` | ||
SELECT | ||
${organizationMemberFields(sql`"om"`)} | ||
FROM | ||
"organization_member" AS "om" | ||
INNER JOIN "users" AS "u" | ||
ON "u"."id" = "om"."user_id" | ||
WHERE | ||
"om"."organization_id" = ${organization.id} | ||
AND lower("u"."email") = lower(${email}) | ||
LIMIT 1 | ||
`; | ||
|
||
const result = await this.pool.maybeOne<unknown>(query); | ||
if (result === null) { | ||
return null; | ||
} | ||
|
||
const membership = RawOrganizationMembershipModel.parse(result); | ||
const mapping = await this.resolveMemberships(organization, [membership]); | ||
return mapping.get(membership.userId) ?? null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add rate limiting to prevent email enumeration attacks.
The findOrganizationMembershipByEmail
method could be vulnerable to email enumeration attacks. Consider implementing rate limiting.
+import rateLimit from 'express-rate-limit';
+
+const emailLookupLimiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 100 // limit each IP to 100 requests per windowMs
+});
async findOrganizationMembershipByEmail(
organization: Organization,
email: string,
): Promise<OrganizationMembership | null> {
+ try {
+ await emailLookupLimiter(organization.id);
+ } catch (error) {
+ this.logger.warn('Rate limit exceeded for email lookup', {
+ organizationId: organization.id,
+ error
+ });
+ throw new Error('Too many email lookup attempts. Please try again later.');
+ }
+
this.logger.debug(
'Find organization membership by email. (organizationId=%s, email=%s)',
organization.id,
email,
);
// ... rest of the method
}
Committable suggestion skipped: line range outside the PR's diff.
<form | ||
onSubmit={ev => { | ||
ev.preventDefault(); | ||
const input: HTMLInputElement = ev.currentTarget.serviceName; | ||
const serviceName = input.value.trim().toLowerCase(); | ||
|
||
if (!serviceName) { | ||
return; | ||
} | ||
|
||
serviceState.addService(serviceName); | ||
input.value = ''; | ||
}} | ||
> | ||
<input | ||
placeholder="Add service by name" | ||
className="mx-2 mt-1 max-w-[70%] border-b text-sm" | ||
name="serviceName" | ||
/> | ||
</form> | ||
</> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add input validation for service names.
The service name input form lacks proper validation beyond trimming and lowercase conversion. Consider adding more robust validation.
+const SERVICE_NAME_REGEX = /^[a-z0-9-]+$/;
+const MAX_SERVICE_NAME_LENGTH = 63;
<form
onSubmit={ev => {
ev.preventDefault();
const input: HTMLInputElement = ev.currentTarget.serviceName;
const serviceName = input.value.trim().toLowerCase();
if (!serviceName) {
return;
}
+ if (!SERVICE_NAME_REGEX.test(serviceName)) {
+ input.setCustomValidity('Service name can only contain lowercase letters, numbers, and hyphens');
+ return;
+ }
+
+ if (serviceName.length > MAX_SERVICE_NAME_LENGTH) {
+ input.setCustomValidity(`Service name cannot exceed ${MAX_SERVICE_NAME_LENGTH} characters`);
+ return;
+ }
serviceState.addService(serviceName);
input.value = '';
}}
>
<input
placeholder="Add service by name"
className="mx-2 mt-1 max-w-[70%] border-b text-sm"
name="serviceName"
+ pattern="[a-z0-9-]+"
+ maxLength={MAX_SERVICE_NAME_LENGTH}
+ title="Service name can only contain lowercase letters, numbers, and hyphens"
/>
</form>
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
<form | |
onSubmit={ev => { | |
ev.preventDefault(); | |
const input: HTMLInputElement = ev.currentTarget.serviceName; | |
const serviceName = input.value.trim().toLowerCase(); | |
if (!serviceName) { | |
return; | |
} | |
serviceState.addService(serviceName); | |
input.value = ''; | |
}} | |
> | |
<input | |
placeholder="Add service by name" | |
className="mx-2 mt-1 max-w-[70%] border-b text-sm" | |
name="serviceName" | |
/> | |
</form> | |
</> | |
<form | |
onSubmit={ev => { | |
ev.preventDefault(); | |
const input: HTMLInputElement = ev.currentTarget.serviceName; | |
const serviceName = input.value.trim().toLowerCase(); | |
if (!serviceName) { | |
return; | |
} | |
if (!SERVICE_NAME_REGEX.test(serviceName)) { | |
input.setCustomValidity('Service name can only contain lowercase letters, numbers, and hyphens'); | |
return; | |
} | |
if (serviceName.length > MAX_SERVICE_NAME_LENGTH) { | |
input.setCustomValidity(`Service name cannot exceed ${MAX_SERVICE_NAME_LENGTH} characters`); | |
return; | |
} | |
serviceState.addService(serviceName); | |
input.value = ''; | |
}} | |
> | |
<input | |
placeholder="Add service by name" | |
className="mx-2 mt-1 max-w-[70%] border-b text-sm" | |
name="serviceName" | |
pattern="[a-z0-9-]+" | |
maxLength={MAX_SERVICE_NAME_LENGTH} | |
title="Service name can only contain lowercase letters, numbers, and hyphens" | |
/> | |
</form> | |
</> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (3)
packages/web/app/src/components/organization/members/permission-selector.tsx (3)
54-91
: Consider optimizing the dependency graph construction.While the current implementation is correct, we can optimize the dependency graph construction by pre-allocating the Map size and using a more functional approach.
Consider this optimization:
- const dependencyGraph = new Map<string, Array<string>>(); + const dependencyGraph = new Map<string, Array<string>>( + organization.availableMemberPermissionGroups.reduce((acc, group) => { + const dependencies = group.permissions + .filter(p => p.dependsOnId) + .map(p => [p.dependsOnId, []] as const); + return acc.concat(dependencies); + }, [] as Array<readonly [string, Array<string>]>) + ); for (const group of organization.availableMemberPermissionGroups) { let selectedPermissionCount = 0; for (const permission of group.permissions) { if (props.selectedPermissionIds.has(permission.id)) { selectedPermissionCount++; } if (permission.dependsOnId) { - let arr = dependencyGraph.get(permission.dependsOnId); - if (!arr) { - arr = []; - dependencyGraph.set(permission.dependsOnId, arr); - } - arr.push(permission.id); + dependencyGraph.get(permission.dependsOnId)?.push(permission.id); }
203-233
: Enhance accessibility of the Select component.The Select component could benefit from improved accessibility attributes.
Consider adding these accessibility enhancements:
<Select disabled={props.isReadOnly || permission.isReadOnly || needsDependency} + aria-label={`Permission level for ${permission.title}`} value={ permission.isReadOnly || props.selectedPermissionIds.has(permission.id) ? 'allow' : 'not-selected' } onValueChange={value => {
167-191
: Consider adding error handling for scroll behavior.The scroll into view logic should handle cases where the element reference is not found.
Add error handling:
onClick={() => { const dependencyPermission = permission.dependsOnId; if (!dependencyPermission) { return; } const element = permissionRefs.current.get(dependencyPermission); if (!element) { + console.warn(`Element reference not found for permission: ${dependencyPermission}`); return; } + try { setOpenAccordions(values => { const groupName = permissionToGroupTitleMapping.get(dependencyPermission); if (groupName && values.includes(groupName) === false) { return [...values, groupName]; } return values; }); setFocusedPermission(dependencyPermission); element.scrollIntoView({ behavior: 'smooth', block: 'center', }); + } catch (error) { + console.error('Failed to scroll to permission:', error); + } }}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
packages/web/app/src/components/organization/members/permission-selector.tsx
(1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
packages/web/app/src/components/organization/members/permission-selector.tsx
[error] 124-124: Missing key property for this element in iterable.
The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.
(lint/correctness/useJsxKeyInIterable)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: build / dockerize (linux/arm64)
🔇 Additional comments (2)
packages/web/app/src/components/organization/members/permission-selector.tsx (2)
22-52
: LGTM! Well-structured GraphQL fragment and type definitions.The GraphQL fragment includes all necessary fields for permission management, and the TypeScript types are properly defined.
1-247
: Overall, excellent implementation of the permission selector component!The component successfully implements granular permission management with proper type safety, performance optimizations, and good UX considerations. The implementation aligns well with the PR objectives.
🧰 Tools
🪛 Biome (1.9.4)
[error] 124-124: Missing key property for this element in iterable.
The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.(lint/correctness/useJsxKeyInIterable)
packages/web/app/src/components/organization/members/permission-selector.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (11)
integration-tests/tests/api/schema/check.spec.ts (2)
1569-1660
: Well-implemented positive test case.The test successfully validates the complete permission chain required for schema approval, demonstrating proper access at all levels (project, target, and permission).
However, consider adding a test case for partial target access (e.g., access to some but not all targets) to ensure proper isolation.
1328-1365
: Consider extracting common setup code.The setup code for creating a failed schema check is duplicated across multiple tests. Consider extracting this into a helper function to improve maintainability and reduce duplication.
Example refactor:
+async function createFailedSchemaCheck(project: Project) { + const { createTargetAccessToken, target } = project; + + // Create a token with write rights + const writeToken = await createTargetAccessToken({}); + + // Publish schema with write rights + const publishResult = await writeToken + .publishSchema({ + sdl: /* GraphQL */ ` + type Query { + ping: String + } + `, + }) + .then(r => r.expectNoGraphQLErrors()); + + // Schema publish should be successful + expect(publishResult.schemaPublish.__typename).toBe('SchemaPublishSuccess'); + + const checkResult = await writeToken + .checkSchema(/* GraphQL */ ` + type Query { + ping: Float + } + `) + .then(r => r.expectNoGraphQLErrors()); + + if (checkResult.schemaCheck.__typename !== 'SchemaCheckError') { + throw new Error('Invalid result: ' + checkResult.schemaCheck.__typename); + } + const schemaCheckId = await checkResult.schemaCheck.schemaCheck?.id; + if (!schemaCheckId) { + throw new Error('Invalid result: ' + JSON.stringify(checkResult, null, 2)); + } + + return { schemaCheckId, target }; +}Also applies to: 1406-1443, 1484-1523, 1575-1612
packages/web/app/src/components/organization/members/roles.tsx (3)
73-73
: Add validation for selectedPermissions array.The schema should validate that the selectedPermissions array is not empty and contains valid permission strings.
- selectedPermissions: z.array(z.string()), + selectedPermissions: z.array(z.string()).min(1, 'At least one permission must be selected').refine( + permissions => permissions.every(p => p.match(/^[a-z]+:[a-z_]+$/)), + 'Invalid permission format' + ),
290-290
: Fix potential double scrollbar issue.Using
overflow-scroll
might lead to double scrollbars. Consider usingoverflow-auto
instead.- <div className="overflow-scroll"> + <div className="overflow-auto">
509-519
: Consolidate duplicate button logic.The submit button logic is duplicated between the selection and confirmation views. Consider extracting it into a reusable component.
const SubmitButton = ({ isSubmitting, label }: { isSubmitting: boolean; label: string }) => ( <Button type="submit" disabled={!form.formState.isValid || isSubmitting || form.formState.disabled} > {isSubmitting ? 'Creating...' : label} </Button> );Also applies to: 539-551
integration-tests/tests/api/project/crud.spec.ts (1)
203-227
: LGTM: Well-structured test for resource restriction.The test effectively validates that a member can be completely restricted from accessing projects using the new granular permission system.
Consider adding an additional assertion to verify that attempting to access a specific project directly (e.g., via
getProject
query) results in an authorization error, ensuring complete access restriction.packages/web/app/src/components/organization/members/permission-selector.tsx (3)
41-52
: Add JSDoc comments to improve type documentation.While the type definitions are well-structured, adding JSDoc comments would improve code documentation and IDE support.
Add documentation like this:
+/** + * Props for the PermissionSelector component + * @property {boolean} [isReadOnly] - Whether the selector is in read-only mode + * @property {FragmentType<typeof PermissionSelector_OrganizationFragment>} organization - Organization data containing permission groups + * @property {ReadonlySet<string>} selectedPermissionIds - Set of currently selected permission IDs + * @property {Function} onSelectedPermissionsChange - Callback when selected permissions change + */ export type PermissionSelectorProps = { isReadOnly?: boolean; organization: FragmentType<typeof PermissionSelector_OrganizationFragment>; selectedPermissionIds: ReadonlySet<string>; onSelectedPermissionsChange: (selectedPermissionIds: ReadonlySet<string>) => void; };
54-91
: Extract permission processing logic into a separate function.The permission processing logic within useMemo is complex and would benefit from being extracted into a separate function for better maintainability and testability.
Consider refactoring like this:
+type ProcessedPermissionData = readonly [ + Array<MembershipPermissionGroup & { selectedPermissionCount: number }>, + Map<string, string>, + Map<string, Array<string>> +]; + +function processPermissionGroups( + groups: AvailableMembershipPermissions, + selectedPermissionIds: ReadonlySet<string> +): ProcessedPermissionData { + const filteredGroups: Array<MembershipPermissionGroup & { selectedPermissionCount: number }> = []; + const permissionToGroupTitleMapping = new Map<string, string>(); + const dependencyGraph = new Map<string, Array<string>>(); + + for (const group of groups) { + let selectedPermissionCount = 0; + // ... rest of the processing logic ... + } + + return [filteredGroups, permissionToGroupTitleMapping, dependencyGraph] as const; +} + export function PermissionSelector(props: PermissionSelectorProps) { const organization = useFragment(PermissionSelector_OrganizationFragment, props.organization); const [groups, permissionToGroupTitleMapping, dependencyGraph] = useMemo( - () => { - const filteredGroups: Array<...> = []; - // ... current processing logic ... - }, + () => processPermissionGroups(organization.availableMemberPermissionGroups, props.selectedPermissionIds), [organization.availableMemberPermissionGroups] );
201-233
: Improve error handling for permission state changes.The permission selection logic should handle edge cases and provide user feedback.
Add error handling and user feedback:
<Select disabled={props.isReadOnly || permission.isReadOnly || needsDependency} value={permission.isReadOnly || props.selectedPermissionIds.has(permission.id) ? 'allow' : 'not-selected' } onValueChange={value => { + try { const dependents = dependencyGraph.get(permission.id) ?? []; if (value === 'allow') { props.onSelectedPermissionsChange( new Set([...props.selectedPermissionIds, permission.id]), ); } else if (value === 'not-selected') { const selectedPermissionIds = new Set(props.selectedPermissionIds); selectedPermissionIds.delete(permission.id); for (const dependent of dependents) { selectedPermissionIds.delete(dependent); } props.onSelectedPermissionsChange(selectedPermissionIds); } setFocusedPermission(null); + } catch (error) { + console.error('Failed to update permission:', error); + // Add toast notification or other user feedback + } }} >integration-tests/testkit/seed.ts (2)
Line range hint
856-872
: Ensure consistent error handling between create and update operations.Both createMemberRole and updateMemberRole have similar error handling patterns, which is good. However, consider extracting the common error handling logic into a shared utility function to maintain consistency and reduce code duplication.
+ function handleMemberRoleErrors(error: { + message: string; + inputErrors?: { + name?: string; + description?: string; + } + }) { + if (error.inputErrors?.name) { + throw new Error(error.inputErrors.name); + } + if (error.inputErrors?.description) { + throw new Error(error.inputErrors.description); + } + throw new Error(error.message); + } async createMemberRole( permissions: Array<string>, options: { useMemberToken?: boolean } = { useMemberToken: false, }, ) { const memberRoleCreationResult = await createMemberRole(/*...*/); if (memberRoleCreationResult.createMemberRole.error) { - if (memberRoleCreationResult.createMemberRole.error.inputErrors?.name) { - throw new Error(memberRoleCreationResult.createMemberRole.error.inputErrors.name); - } - if (memberRoleCreationResult.createMemberRole.error.inputErrors?.description) { - throw new Error(memberRoleCreationResult.createMemberRole.error.inputErrors.description); - } - throw new Error(memberRoleCreationResult.createMemberRole.error.message); + handleMemberRoleErrors(memberRoleCreationResult.createMemberRole.error); } // ... } async updateMemberRole( role: { id: string; name: string; description: string }, permissions: Array<string>, options: { useMemberToken?: boolean } = { useMemberToken: false, }, ) { const memberRoleUpdateResult = await updateMemberRole(/*...*/); if (memberRoleUpdateResult.updateMemberRole.error) { - if (memberRoleUpdateResult.updateMemberRole.error.inputErrors?.name) { - throw new Error(memberRoleUpdateResult.updateMemberRole.error.inputErrors.name); - } - if (memberRoleUpdateResult.updateMemberRole.error.inputErrors?.description) { - throw new Error(memberRoleUpdateResult.updateMemberRole.error.inputErrors.description); - } - throw new Error(memberRoleUpdateResult.updateMemberRole.error.message); + handleMemberRoleErrors(memberRoleUpdateResult.updateMemberRole.error); } // ... }Also applies to: 911-922
821-824
: Consider documenting the default resource assignment behavior.The default resource assignment using
ResourceAssignmentMode.All
with an empty projects array is a significant behavior that should be documented for better maintainability.resources: input.resources ?? { + // Default to granting access to all resources when no specific projects are provided mode: GraphQLSchema.ResourceAssignmentMode.All, projects: [], },
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
integration-tests/testkit/seed.ts
(8 hunks)integration-tests/tests/api/project/crud.spec.ts
(2 hunks)integration-tests/tests/api/schema/check.spec.ts
(2 hunks)packages/web/app/src/components/organization/members/permission-selector.tsx
(1 hunks)packages/web/app/src/components/organization/members/roles.tsx
(16 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (2)
- GitHub Check: build / dockerize (linux/arm64)
- GitHub Check: build / dockerize (linux/amd64)
🔇 Additional comments (12)
integration-tests/tests/api/schema/check.spec.ts (4)
1-5
: LGTM! Import changes align with granular permissions implementation.The addition of
ResourceAssignmentMode
import supports the new granular permission assignment feature.
1322-1398
: Well-structured test for default role permissions.The test effectively verifies that users with default roles cannot approve schema checks without proper permissions. Good error validation and clear setup structure.
1400-1476
: Comprehensive test for project-level access control.The test properly validates that even with schema approval permissions, access is denied without project resource access. Good separation of concerns between permission types.
1478-1567
: Thorough test for target-level access control.The test effectively demonstrates the granular nature of the permissions system by showing that project access alone is insufficient without target access.
packages/web/app/src/components/organization/members/roles.tsx (2)
590-590
: LGTM! Clean implementation of the permissions field.The transition from scopes to permissions is handled cleanly in the row component.
136-139
:⚠️ Potential issueAdd missing form dependency to useCallback.
The callback uses the form object but doesn't include it in the dependencies array, which could lead to stale closures.
const onChangeSelectedPermissions = useCallback((permissions: ReadonlySet<string>) => { setSelectedPermissions(new Set(permissions)); form.setValue('selectedPermissions', [...permissions]); - }, []); + }, [form]);Likely invalid or redundant comment.
integration-tests/tests/api/project/crud.spec.ts (2)
1-1
: LGTM: Import changes align with new test requirements.The addition of
ResourceAssignmentMode
import is necessary for the new permission-based tests.
229-262
: LGTM: Comprehensive test for granular project access.The test effectively validates selective project access using the new permission system.
Let's verify if similar test patterns exist in the codebase:
Consider adding test cases for:
- Invalid project IDs in the resources configuration
- Mixing granular and all modes for different resources
- Edge cases around permission inheritance
✅ Verification successful
Test implementation follows established patterns in the codebase
The test case correctly implements resource assignment following the same patterns used in other permission-related tests, particularly in
schema/check.spec.ts
. The original review comment's suggestions for additional test cases complement the existing coverage, as the codebase already includes tests for various resource assignment scenarios.🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Search for similar test patterns using ResourceAssignmentMode rg -l "ResourceAssignmentMode" "integration-tests/tests" # Look for other permission-related tests rg -l "assignMemberRole.*resources.*mode" "integration-tests/tests"Length of output: 220
Script:
#!/bin/bash # Check the content of schema/check.spec.ts for ResourceAssignmentMode usage rg -A 5 -B 5 "ResourceAssignmentMode" "integration-tests/tests/api/schema/check.spec.ts" # Look for test cases involving resource assignment in both files ast-grep --pattern 'test.concurrent($_, async () => { $$$ ResourceAssignmentMode $$$ })'Length of output: 2544
packages/web/app/src/components/organization/members/permission-selector.tsx (1)
1-39
: LGTM! Well-structured imports and GraphQL fragment definition.The imports are logically organized, and the GraphQL fragment effectively captures all necessary fields for permission management, including dependencies and warnings.
integration-tests/testkit/seed.ts (3)
51-51
: LGTM! Import added for new permission types.The new import for GraphQLSchema types aligns with the PR's objective of implementing granular permissions.
189-192
: LGTM! Enhanced flexibility with optional token parameter.The projects method now accepts an optional token parameter with a default value of ownerToken, improving reusability and testing scenarios.
Line range hint
810-824
: Consider adding validation for resources parameter.While the implementation correctly adds support for granular resource assignment, it would be beneficial to add validation for the resources parameter to ensure the provided projects array contains valid project IDs.
@@ -504,97 +435,122 @@ function OrganizationMemberRoleCreator(props: { | |||
|
|||
return ( | |||
<Form {...form}> | |||
<form onSubmit={form.handleSubmit(onSubmit)}> | |||
<form> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add missing onSubmit handler to the form element.
The form element should have an onSubmit handler to prevent default form submission.
- <form>
+ <form onSubmit={form.handleSubmit(onSubmit)}>
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
<form> | |
<form onSubmit={form.handleSubmit(onSubmit)}> |
packages/web/app/src/components/organization/members/permission-selector.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
packages/web/app/src/components/organization/members/permission-selector.tsx (2)
54-91
: Consider optimizing data processing performance.The current implementation rebuilds the dependency graph on every render within useMemo. Consider these optimizations:
- Move the dependency graph construction to a utility function
- Consolidate the iterations over permissions
+ // New utility function + function buildPermissionMaps(groups: AvailableMembershipPermissions) { + const permissionToGroupTitleMapping = new Map<string, string>(); + const dependencyGraph = new Map<string, Array<string>>(); + + for (const group of groups) { + for (const permission of group.permissions) { + if (permission.dependsOnId) { + const arr = dependencyGraph.get(permission.dependsOnId) ?? []; + dependencyGraph.set(permission.dependsOnId, [...arr, permission.id]); + } + permissionToGroupTitleMapping.set(permission.id, group.title); + } + } + + return [permissionToGroupTitleMapping, dependencyGraph] as const; + } export function PermissionSelector(props: PermissionSelectorProps) { const organization = useFragment(PermissionSelector_OrganizationFragment, props.organization); + const [permissionToGroupTitleMapping, dependencyGraph] = useMemo( + () => buildPermissionMaps(organization.availableMemberPermissionGroups), + [organization.availableMemberPermissionGroups] + ); const groups = useMemo(() => { return organization.availableMemberPermissionGroups.map(group => ({ ...group, selectedPermissionCount: group.permissions.filter(permission => props.selectedPermissionIds.has(permission.id) ).length })); }, [organization.availableMemberPermissionGroups, props.selectedPermissionIds]);
144-204
: Enhance tooltip accessibility.The tooltips for warnings and dependencies need better ARIA support for screen readers.
<TooltipProvider delayDuration={0}> - <Tooltip> + <Tooltip> <TooltipTrigger> - <TriangleAlert className="text-yellow-700" /> + <TriangleAlert + className="text-yellow-700" + aria-label={`Warning: ${permission.warning}`} + role="img" + /> </TooltipTrigger> - <TooltipContent>{permission.warning}</TooltipContent> + <TooltipContent role="tooltip">{permission.warning}</TooltipContent> </Tooltip> </TooltipProvider>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
packages/web/app/src/components/organization/members/permission-selector.tsx
(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (8)
- GitHub Check: typescript / typecheck
- GitHub Check: storybook-preview / deployment
- GitHub Check: static-analysis / analyze (typescript)
- GitHub Check: static-analysis / analyze (javascript)
- GitHub Check: build / dockerize (linux/arm64)
- GitHub Check: code-style / eslint-and-prettier
- GitHub Check: build / dockerize (linux/amd64)
- GitHub Check: alpha / cli-artifacts
🔇 Additional comments (4)
packages/web/app/src/components/organization/members/permission-selector.tsx (4)
1-39
: LGTM! Well-structured GraphQL fragment and imports.The GraphQL fragment captures all necessary fields for granular permission management, and the imports are well-organized.
41-52
: LGTM! Type-safe and immutable interfaces.Good use of TypeScript features with ReadonlySet for immutability and clear prop definitions.
93-250
: 🛠️ Refactor suggestionAdd error boundaries and enhance keyboard navigation.
The UI implementation would benefit from improved error handling and accessibility:
- Add error boundaries to handle potential runtime errors
- Enhance keyboard navigation
- Improve focus management after scrolling
+ import { Component, ErrorInfo, ReactNode } from 'react'; + + class PermissionSelectorErrorBoundary extends Component< + { children: ReactNode }, + { hasError: boolean } + > { + state = { hasError: false }; + + static getDerivedStateFromError() { + return { hasError: true }; + } + + componentDidError(error: Error, errorInfo: ErrorInfo) { + console.error('PermissionSelector failed:', error, errorInfo); + } + + render() { + if (this.state.hasError) { + return ( + <div className="text-red-500"> + Failed to load permission selector. Please try again. + </div> + ); + } + return this.props.children; + } + } export function PermissionSelector(props: PermissionSelectorProps) { // ... existing code ... return ( + <PermissionSelectorErrorBoundary> <Accordion + onKeyDown={e => { + if (e.key === 'Escape') { + setFocusedPermission(null); + } + }} // ... existing props ... > {/* ... existing content ... */} + {/* Add focus management after scroll */} + useEffect(() => { + if (focusedPermission) { + const element = permissionRefs.current.get(focusedPermission); + if (element) { + const selectTrigger = element.querySelector('[role="combobox"]'); + if (selectTrigger instanceof HTMLElement) { + selectTrigger.focus(); + } + } + } + }, [focusedPermission]); </Accordion> + </PermissionSelectorErrorBoundary> ); }Likely invalid or redundant comment.
54-250
: Implementation aligns well with PR objectives.The PermissionSelector component successfully implements the granular permission management system described in the PR objectives. It handles:
- Granular permission assignments
- Permission dependencies
- Role-based access control
- Legacy scope compatibility
✅ Verification successful
Implementation correctly handles legacy scope migration and new permission system
The PermissionSelector component successfully implements the new permission model while maintaining backward compatibility through proper migration of legacy scopes. The migration path and permission dependency handling are well-tested and correctly implemented.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Verify that all permission-related components use the new permission strings # and that there are no remaining legacy scope references # Search for legacy scope references echo "Checking for legacy scope references..." rg -i "scope" --type ts --type tsx # Search for new permission string usage echo "Checking for new permission string usage..." rg -i "permission" --type ts --type tsxLength of output: 316
Script:
#!/bin/bash # Search for legacy scope references with context echo "Checking for legacy scope references..." rg -i "legacy.*scope|scope.*legacy" -C 2 # Search for scope-related patterns echo "Checking for general scope usage..." rg -i "\bscope" -C 2 # Search for permission-related patterns echo "Checking for permission string usage..." rg -i "permission(Id|String|Group)" -C 2 # Check specific files related to permissions echo "Checking permission-related files..." fd -e ts -e tsx "permission" --exec rg -l "scope|permission"Length of output: 68257
Description
This PR introduces using the new permission strings for organization member roles instead of the legacy scopes.
Previously, we used scopes throughout the whole codebase which were replaced with permission strings in #5674.
With these changes, users now assign permissions to member roles instead of scopes. Existing legacy scopes within the database are automatically converted to the new permissions within a small translation layer.
On the Hive Console Dashboard, we now have a new permission picker.
View before
Previously we had the "legacy" scopes, that allowed us to do a group of actions, instead of assigning specific actions to a user role.
Our customers expressed the need to require more granularity over which permissions to assign to a role.
Including a permission confirmation screen.
Additional noteworthy changes:
Storage
class and moves methods for reading and updating theorganization_member
andorganization_member_roles
PG tables' to dedicated classes.Admin
andViewer
within an organization are now maintained within the code; if we introduce new permissions we can do so by updating the permission definitions of those roles instead of having to run a database migrationViewer
role have been downgraded, so they can only read, but no longer create targets or projects. (This needs to be communicated via the product update)member:assignRole
,member:modifyRole
, ormember:manageInvites
can always assign any role to any user, except the organization owner/admin. Existing legacy scope permissions will no longer be translated to the corresponding permissions. (This needs to be communicated via the product update)Follow-up PR for documentation update & Product Update:
#6384
Checklist
Summary by CodeRabbit
Release Notes: Hive Platform Permission System Update
New Features
Breaking Changes
Improvements
Migration Recommendations