Skip to content
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

Merged
merged 58 commits into from
Jan 29, 2025

Conversation

n1ru4l
Copy link
Contributor

@n1ru4l n1ru4l commented Dec 30, 2024

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.

image
image

Including a permission confirmation screen.

image

Additional noteworthy changes:

  • Breaks down the Storage class and moves methods for reading and updating the organization_member and organization_member_roles PG tables' to dedicated classes.
  • Permissions for the pre-defined roles Admin and Viewer 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 migration
  • Permissions of the pre-defined Viewer 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)
  • A organization member with member:assignRole, member:modifyRole, or member: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

  • Input validation
  • Output encoding
  • Authentication management
  • Session management
  • Access control
  • Cryptographic practices
  • Error handling and logging
  • Data protection
  • Communication security
  • System configuration
  • Database security
  • File management
  • Memory management
  • Testing

Summary by CodeRabbit

Release Notes: Hive Platform Permission System Update

  • New Features

    • Introduced a granular permission system for organization member roles.
    • Replaced legacy access scopes with a more flexible permissions model.
    • Added resource-based access control for projects, targets, and services.
    • Enhanced permission management with a structured approach for defining and validating permissions.
  • Breaking Changes

    • Default 'Viewer' role now has reduced capabilities.
    • Removed ability to create targets or projects for default roles.
    • Reorganized role assignment and management processes.
    • Eliminated previous access scope definitions, simplifying the overall permission structure.
  • Improvements

    • Simplified role assignment interface.
    • Improved resource-level permission handling.
    • Streamlined testing and validation processes for permissions.
  • Migration Recommendations

    • Review and update existing organization member roles.
    • Carefully reassign permissions to appropriate roles.
    • Limit administrative rights to designated admin roles.

Copy link
Contributor

github-actions bot commented Dec 30, 2024

🚀 Snapshot Release (alpha)

The latest changes of this PR are available as alpha on npm (based on the declared changesets):

Package Version Info
hive 5.0.0-alpha-20250128144605-d06ba598fd8ecc3a7d621a9877229a596f581d55 npm ↗︎ unpkg ↗︎

Copy link
Contributor

github-actions bot commented Dec 30, 2024

🐋 This PR was built and pushed to the following Docker images:

Targets: build

Platforms: linux/amd64

Image Tag: d06ba598fd8ecc3a7d621a9877229a596f581d55

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"
}
}

@n1ru4l n1ru4l force-pushed the feat-permission-member-roles branch 2 times, most recently from df3bd63 to d410863 Compare January 3, 2025 10:05
Copy link
Contributor

github-actions bot commented Jan 3, 2025

📚 Storybook Deployment

The latest changes are available as preview in: https://6b531d36.hive-storybook.pages.dev

@n1ru4l n1ru4l force-pushed the feat-permission-member-roles branch 2 times, most recently from 5fdb9f0 to 8fc0739 Compare January 13, 2025 10:53
@n1ru4l n1ru4l changed the title wip: granular permissions feat: granular permission assignment Jan 13, 2025
@n1ru4l n1ru4l changed the title feat: granular permission assignment feat: granular permission assignment for organization members Jan 13, 2025
@n1ru4l n1ru4l mentioned this pull request Jan 14, 2025
14 tasks
@n1ru4l n1ru4l force-pushed the feat-permission-member-roles branch 3 times, most recently from a8eadc4 to d3810a9 Compare January 17, 2025 11:29
@n1ru4l n1ru4l force-pushed the feat-permission-member-roles branch from eb82292 to cb29617 Compare January 24, 2025 15:18
@theguild-bot theguild-bot deployed to development January 28, 2025 10:04 Active
@dotansimha
Copy link
Member

@coderabbitai summary

Copy link

coderabbitai bot commented Jan 28, 2025

✅ Actions performed

Summary regeneration triggered.

Copy link

coderabbitai bot commented Jan 28, 2025

Walkthrough

This 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

File/Group Change Summary
GraphQL Schema Replaced access scopes with a new permissions system, added resource assignment inputs and types. Introduced new types for permissions and permission groups.
Authorization Logic Simplified permission checks, removed legacy scope-based access control, and restructured permission management.
Database Migrations Added columns for permissions and assigned_resources in organization member tables.
Frontend Components Introduced new components for permission and resource selection, updated role management interfaces, and removed legacy components related to access scopes.

Sequence Diagram

sequenceDiagram
    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
Loading

Poem

🐰 Permissions Dance

In Hive's grand hall of access and might,
Roles shimmer with a granular light
Resources tagged, permissions clear
No more broad strokes, precision is here!

Hop hop hooray for fine-grained control! 🔐


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a 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 vs billing:describe
  • targetAccessToken:modify vs target:modifySettings

Consider standardizing to either:

  1. <resource>:<action> (e.g., target:modifyAccessToken)
  2. <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:

  1. Making it a const and building it at compile-time
  2. 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:

  1. Adding a comment explaining the transformation
  2. 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:

  1. Adding more specific error messages for different failure cases
  2. 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:

  1. Make the error message more descriptive (e.g., "Role not found for invitation").
  2. 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 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)


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:

  1. Attempting to access a restricted project
  2. 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 and ServiceResourceAssignmentInput accept string inputs for names. Consider adding:

  1. Pattern validation for allowed characters
  2. Length restrictions
  3. 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

📥 Commits

Reviewing files that changed from the base of the PR and between ba43c93 and f182dd8.

📒 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> to MemberResolvers 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 ts

Length 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:

  1. Adding explicit null checks before rendering
  2. Using type narrowing with the conditional check
  3. 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 and RegistryAccessTokens_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 the viewerCanModifyTargetAccessToken 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 tsx

Length 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 10

Length 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 suggestion

Add 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 in OrganizationManager.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 through assertPerformAction 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:

  1. All clients are updated to handle the new permission structure
  2. 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:

  1. Permission inheritance and dependencies
  2. Permission conflicts resolution
  3. 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 ts

Length 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:

  1. Clear ownership indication
  2. Resource assignment integration
  3. Proper permission checks for member management

522-531: LGTM! Comprehensive resource assignment structure.

The ResourceAssignmentInput type provides a flexible way to:

  1. Assign permissions at organization level
  2. Control access to specific projects
  3. 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 and findTargetsByIds are well-defined and follow the interface's existing patterns. The use of Map 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 as findProjectsByIds 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 the created_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 of ownerId improves ownership tracking.

The new ownerId property in the Organization interface explicitly tracks organization ownership, which aligns with the granular permission system being introduced.


205-205: LGTM! The change from role to roleId improves data modeling.

Replacing role: OrganizationMemberRole with roleId: string in the OrganizationInvitation 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.

Comment on lines +269 to +299
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;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Comment on lines +686 to +706
<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>
</>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
<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>
</>

.changeset/tall-islands-occur.md Show resolved Hide resolved
integration-tests/tests/api/project/crud.spec.ts Outdated Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between f182dd8 and d2a31c5.

📒 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)

Copy link

@coderabbitai coderabbitai bot left a 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 using overflow-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

📥 Commits

Reviewing files that changed from the base of the PR and between d2a31c5 and d679387.

📒 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 issue

Add 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:

  1. Invalid project IDs in the resources configuration
  2. Mixing granular and all modes for different resources
  3. 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>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
<form>
<form onSubmit={form.handleSubmit(onSubmit)}>

Copy link

@coderabbitai coderabbitai bot left a 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:

  1. Move the dependency graph construction to a utility function
  2. 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

📥 Commits

Reviewing files that changed from the base of the PR and between d679387 and d06ba59.

📒 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 suggestion

Add error boundaries and enhance keyboard navigation.

The UI implementation would benefit from improved error handling and accessibility:

  1. Add error boundaries to handle potential runtime errors
  2. Enhance keyboard navigation
  3. 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 tsx

Length 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

@n1ru4l n1ru4l merged commit b7e4052 into main Jan 29, 2025
30 checks passed
@n1ru4l n1ru4l deleted the feat-permission-member-roles branch January 29, 2025 09:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

5 participants