diff --git a/.github/workflows/ci-build-deploy.yaml b/.github/workflows/ci-build-deploy.yaml index 38157814a..4f9ff74a1 100644 --- a/.github/workflows/ci-build-deploy.yaml +++ b/.github/workflows/ci-build-deploy.yaml @@ -177,7 +177,7 @@ jobs: client-secret: ${{ secrets.OIDC_CLIENT_SECRET }} oidc-issuer-url: ${{ secrets.OIDC_ISSUER }} redirect-url: https://api-services-portal-${{ steps.set-deploy-id.outputs.DEPLOY_ID }}.apps.silver.devops.gov.bc.ca/oauth2/callback - skip-auth-regex: '/health|/public|/docs|/redirect|/_next|/images|/devportal|/manager|/ds/api|/signout|^[/]$' + skip-auth-regex: '/health|/public|/docs|/redirect|/_next|/images|/devportal|/manager|/feed/|/ds/api|/signout|^[/]$' whitelist-domain: authz-apps-gov-bc-ca.dev.api.gov.bc.ca skip-provider-button: 'true' profile-url: ${{ secrets.OIDC_ISSUER }}/protocol/openid-connect/userinfo diff --git a/.github/workflows/ci-build-feeders.yaml b/.github/workflows/ci-build-feeders.yaml index 080cb1d6f..e16bc7f66 100644 --- a/.github/workflows/ci-build-feeders.yaml +++ b/.github/workflows/ci-build-feeders.yaml @@ -125,6 +125,8 @@ jobs: name: proto-asp-${{ steps.set-deploy-id.outputs.DEPLOY_ID }} env: + TZ: + value: 'America/Los_Angeles' LOG_FEEDS: value: 'false' WORKING_PATH: diff --git a/README.md b/README.md index ab4783902..167e3afb7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # API Services Portal +[![Lifecycle:Maturing](https://img.shields.io/badge/Lifecycle-Maturing-007EC6)](https://github.com/bcgov/repomountie/blob/master/doc/lifecycle-badges.md) + ## Introduction The `API Services Portal` is a frontend for API Providers to manage the lifecycle of their APIs and for Developers to discover and access these APIs. It works in combination with the Kong Community Edition Gateway and Keycloak IAM solution. diff --git a/src/authz/matrix.csv b/src/authz/matrix.csv index 18a3a520f..da8342163 100644 --- a/src/authz/matrix.csv +++ b/src/authz/matrix.csv @@ -1,144 +1,146 @@ -ID,matchOneOfBaseQueryName,matchQueryName,matchListKey,matchOperation,matchOneOfOperation,matchOneOfFieldKey,matchNotOneOfFieldKey,matchUserNS,inRole,matchOneOfScope,matchOneOfRole,result,filters -,,,,,"update,create,delete",,,,aps-admin,,,allow, -API Owner Role Rules,,,AccessRequest,update,,isApproved,,,access-manager,,,allow, -API Owner Role Rules,,,AccessRequest,,"create,read",isApproved,,,api-owner,,,allow, -API Owner Role Rules,,,AccessRequest,read,,,,,api-owner,,,allow, -API Owner Role Rules,,,AccessRequest,,"create,update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,Activity,read,,,,,,,"api-owner,provider-user",allow,filterByUserNS -API Owner Role Rules,,,Alert,,read,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Alert,,"create,update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,Application,,"read,update,delete",,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Application,,create,,,,,,"api-owner,provider-user",allow, -,,BusinessProfile,,,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Content,read,,,,,api-owner,,,allow, -API Owner Role Rules,,,CredentialIssuer,read,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,CredentialIssuer,,"update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,CredentialIssuer,create,,,,,api-owner,,,allow, -API Owner Role Rules,,,Dataset,read,,,,,api-owner,,,allow, -API Owner Role Rules,,,Environment,create,,active,,,api-owner,,,deny, -API Owner Role Rules,,,Environment,,"update,delete,read",active,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Environment,read,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Environment,create,,,,,api-owner,,,allow, -API Owner Role Rules,,,Environment,,"update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,GatewayConsumer,,"read,update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,GatewayConsumer,create,,,,,api-owner,,,allow, -API Owner Role Rules,,,GatewayGroup,read,,,,,api-owner,,,allow, -API Owner Role Rules,,,GatewayGroup,,"update,delete,create",,,,api-owner,,,allow, -API Owner Role Rules,,,GatewayPlugin,read,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,GatewayRoute,read,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,GatewayService,read,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Group,create,,,,,api-owner,,,allow, -API Owner Role Rules,,,Group,,"update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,Group,read,,,,,api-owner,,,allow, -API Owner Role Rules,,,Legal,read,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Legal,,"update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,MemberRole,create,,,,,api-owner,,,allow, -API Owner Role Rules,,,MemberRole,,"update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,MemberRole,read,,,,,api-owner,,,allow, -API Owner Role Rules,,,Metric,,read,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Namespace,,read,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Namespace,create,,,,,api-owner,,,allow, -API Owner Role Rules,,,Namespace,,"update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,Organization,read,,,,,api-owner,,,allow, -API Owner Role Rules,,,OrganizationUnit,read,,,,,api-owner,,,allow, -API Owner Role Rules,,,Product,update,,namespace,,,,,,deny, -API Owner Role Rules,,,Product,read,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,Product,create,,,,,api-owner,,,allow, -API Owner Role Rules,,,Product,,"update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,ServiceAccess,create,,,,,api-owner,,,allow, -API Owner Role Rules,,,ServiceAccess,,"update,delete",,,,api-owner,,,allow, -API Owner Role Rules,,,ServiceAccess,read,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,,TemporaryIdentity,read,,,,,,,"api-owner,provider-user",allow,filterByTemporaryIdentity -API Owner Role Rules,,,User,read,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,getGatewayConsumerPlugins,,,,,,,api-owner,,,allow, -API Owner Role Rules,,deleteGatewayConsumerPlugin,,,,,,,api-owner,,,allow, -API Owner Role Rules,,createGatewayConsumerPlugin,,,,,,,api-owner,,,allow, -API Owner Role Rules,,getPermissionTicketsForResource,,,,,,,api-owner,,,allow, -API Owner Role Rules,,allPermissionTickets,,,,,,,api-owner,,,allow, -API Owner Role Rules,,updateConsumerGroupMembership,,,,,,,api-owner,,,allow,filterByPackageNS -API Owner Role Rules,,consumerScopesAndRoles,,,,,,,api-owner,,,allow,filterByPackageNS -API Owner Role Rules,,linkConsumerToNamespace,,,,,,,api-owner,,,allow, -API Owner Role Rules,,updateConsumerRoleAssignment,,,,,,,api-owner,,,allow,filterByPackageNS -Portal User Namespaces,,allNamespaces,,,,,,,portal-user,,,allow, -API Owner Role Rules,,createNamespace,,,,,,,idir-user,,,allow, -API Owner Role Rules,,deleteNamespace,,,,,,,api-owner,,,allow, -API Owner Role Rules,,currentNamespace,,,,,,,api-owner,,,allow, -API Owner Role Rules,,grantPermissions,,,,,,,api-owner,,,allow, -API Owner Role Rules,,revokePermissions,,,,,,,api-owner,,,allow, -API Owner Role Rules,,approvePermissions,,,,,,,api-owner,,,allow, -API Owner Role Rules,,getResourceSet,,,,,,,api-owner,,,allow, -API Owner Role Rules,,allResourceSets,,,,,,,api-owner,,,allow, -API Owner Role Rules,,getUmaPoliciesForResource,,,,,,,api-owner,,,allow, -API Owner Role Rules,,createUmaPolicy,,,,,,,api-owner,,,allow, -API Owner Role Rules,,deleteUmaPolicy,,,,,,,api-owner,,,allow, -API Owner Role Rules,,getServiceAccounts,,,,,,,,,"api-owner,provider-user",allow, -API Owner Role Rules,,createServiceAccount,,,,,,,api-owner,,,allow, -API Owner Role Rules,,deleteServiceAccount,,,,,,,api-owner,,,allow, -Developer Role Rules,,myServiceAccesses,,,,,,,portal-user,,,allow,filterByAppOwner -Developer Role Rules,,myApplications,,,,,,,portal-user,,,allow,filterByOwner -Developer Role Rules,,allDiscoverableProducts,,,,,,,portal-user,,,allow,filterByActiveEnvironment -Developer Role Rules,,allDiscoverableContents,,,,,,,portal-user,,,allow, -Developer Role Rules,,DiscoverableProduct,,,,,,,portal-user,,,allow, -Developer Role Rules,,,Product,read,,,,,portal-user,,,allow, -Developer Role Rules,,,Environment,read,,,,,portal-user,,,allow, -Developer Role Rules,,,Dataset,read,,,,,portal-user,,,allow, -Developer Role Rules,,,Organization,read,,,,,portal-user,,,allow, -Developer Role Rules,,,OrganizationUnit,read,,,,,portal-user,,,allow, -Developer Role Rules,,,GatewayService,read,,,,,portal-user,,,allow, -Developer Role Rules,,,GatewayRoute,read,,,,,portal-user,,,allow, -Developer Role Rules,,,GatewayConsumer,read,,,,,portal-user,,,allow, -Developer Role Rules,,,Legal,read,,,,,portal-user,,,allow, -Developer Role Rules,,,TemporaryIdentity,read,,,,,portal-user,,,allow,filterByTemporaryIdentity -Developer Role Rules,,,User,read,,,,,portal-user,,,allow,filterBySelf -Developer Role Rules,,,CredentialIssuer,read,,,,,portal-user,,,allow, -Developer Role Rules,,,AccessRequest,,create,,,,portal-user,,,allow, -Developer Role Rules,,,AccessRequest,,"update,read",,,,portal-user,,,allow,filterByRequestor -Developer Role Rules,,,Application,create,,,,,portal-user,,,allow, -Developer Role Rules,,,Application,,"read,delete,update",,,,portal-user,,,allow,filterByOwner -Developer Role Rules,,,ServiceAccess,delete,,,,,portal-user,,,allow,filterByAppOwner -Developer Role Rules,,,ServiceAccess,read,,,,,portal-user,,,allow,filterByAppOwner -Developer Role Rules - Fields,,,Product,read,,"id,name,dataset,organization,organizationUnit,environments",,,portal-user,,,allow, -Developer Role Rules - Fields,,,Environment,read,,"name,active,flow,services,legal,product,credentialIssuer",,,portal-user,,,allow, -Developer Role Rules - Fields,,,Dataset,read,,,"isInCatalog,extSource,extForeignKey,extRecordHash",,portal-user,,,allow, -Developer Role Rules - Fields,,,Organization,read,,title,,,portal-user,,,allow, -Developer Role Rules - Fields,,,OrganizationUnit,read,,title,,,portal-user,,,allow, -Developer Role Rules - Fields,,,GatewayService,read,,"name,routes",,,portal-user,,,allow, -Developer Role Rules - Fields,,,GatewayRoute,read,,"name,methods,hosts,paths",,,portal-user,,,allow, -Developer Role Rules - Fields,,,Legal,read,,"title,description,link",,,portal-user,,,allow, -Developer Role Rules - Fields,,,TemporaryIdentity,read,,"id,userId,name,email,username",,,portal-user,,,allow, -Developer Role Rules - Fields,,,Content,read,,"slug,title,description",,,portal-user,,,allow, -Developer Role Rules - Fields,,,Application,create,,"name,description",,,portal-user,,,allow, -Developer Role Rules - Fields,,,Application,read,,"appId,name,owner",,,portal-user,,,allow, -Developer Role Rules - Fields,,,User,read,,"name,username,email,legalsAgreed",,,portal-user,,,allow, -Developer Role Rules - Fields,,,ServiceAccess,read,,"name,active,productEnvironment",,,portal-user,,,allow, -Developer Role Rules - Fields,,,CredentialIssuer,read,,"id,name,flow,resourceType",,,portal-user,,,allow, -Developer Role Rules - Fields,,,AccessRequest,create,,"name,controls,requestor,application,productEnvironment",,,portal-user,,,allow, -Developer Role Rules - Fields,,,AccessRequest,,"read,update",credential,,,portal-user,,,allow, -Developer Role Rules - Fields,,,AccessRequest,read,,"name,controls,application,productEnvironment,isIssued",,,portal-user,,,allow, -Developer Role Rules - Fields,,,ServiceAccess,read,,"consumer,application",,,portal-user,,,allow, -API Owner Role - All Fields,,,,,read,,*,,,,"api-owner,provider-user",allow, -API Owner Role - All Fields,,,,,"update,create",,*,,api-owner,,,allow, -Portal User,,DiscoverableProduct,,,,,,,portal-user,,,allow, -Portal User,,myServiceAccesses,,,,,,,portal-user,,,allow,filterByAppOwner -Portal User,,myApplications,,,,,,,portal-user,,,allow,filterByOwner -API Owner Role Rules,,allGatewayServicesByNamespace,,,,,,,,,"api-owner,provider-user",allow,filterByUserNS -API Owner Role Rules,,allProductsByNamespace,,,,,,,,,"api-owner,provider-user",allow,filterByUserNS -API Owner Role Rules,,allAccessRequestsByNamespace,,,,,,,api-owner,,,allow,filterByEnvironmentPackageNS -API Owner Role Rules,,allServiceAccessesByNamespace,,,,,,,api-owner,,,allow,filterByEnvironmentProductNSOrNS -API Owner Role Rules,,allNamespaceServiceAccounts,,,,,,,,,"api-owner,provider-user",allow,filterByUserNS -API Owner Role Rules,,allCredentialIssuersByNamespace,,,,,,,,,"api-owner,provider-user",allow,filterByUserNS -,,getUmaPoliciesByResourceName,,,,,,,api-owner,,,allow, -,,getResourceOwners,,,,,,,api-owner,,,allow, -Portal User or Guest,,allDiscoverableContents,,,,,,,,,"portal-user,guest",allow,filterByNamespaceOrPublic -Portal User or Guest,,allDiscoverableProducts,,,,,,,,,"portal-user,guest",allow,filterByActiveEnvironment -Portal User or Guest,,allProducts,,,,,,,,,"portal-user,guest",allow, -Portal User or Guest,,environments,,,,,,,,,"portal-user,guest",allow, -Portal User or Guest,,allDatasets,,,,,,,,,"portal-user,guest",allow, -Portal User or Guest,,Environment,,,,,,,,,"portal-user,guest",allow, -Portal User or Guest,,,Environment,read,,"name,active,flow,services,legal,product,credentialIssuer",,,,,"portal-user,guest",allow, -Portal User or Guest,,allContents,,,,,,,,,"portal-user,guest",allow, -Portal User or Guest,,DiscoverableProduct,,,,,,,,,"portal-user,guest",allow, -Portal User or Guest,,services,,,,,,,,,"portal-user,guest",allow, -ALL USERS,,mySelf,,,,,,,,,,allow,filterBySelf -ALL USERS,,acceptLegal,,,,,,,,,,allow, +ID,matchOneOfBaseQueryName,matchQueryName,matchListKey,matchOperation,matchOneOfOperation,matchOneOfFieldKey,matchNotOneOfFieldKey,matchUserNS,inRole,matchOneOfScope,matchOneOfRole,result,filters +,,,,,"update,create,delete",,,,aps-admin,,,allow, +API Owner Role Rules,,,AccessRequest,update,,isApproved,,,access-manager,,,allow, +API Owner Role Rules,,,AccessRequest,,"create,read",isApproved,,,api-owner,,,allow, +API Owner Role Rules,,,AccessRequest,read,,,,,api-owner,,,allow, +API Owner Role Rules,,,AccessRequest,,"create,update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,Activity,read,,,,,,,"api-owner,provider-user",allow,filterByUserNS +API Owner Role Rules,,,Alert,,read,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Alert,,"create,update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,Application,,"read,update,delete",,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Application,,create,,,,,,"api-owner,provider-user",allow, +,,BusinessProfile,,,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Content,read,,,,,api-owner,,,allow, +API Owner Role Rules,,,CredentialIssuer,read,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,CredentialIssuer,,"update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,CredentialIssuer,create,,,,,api-owner,,,allow, +API Owner Role Rules,,,Dataset,read,,,,,api-owner,,,allow, +API Owner Role Rules,,,Environment,create,,active,,,api-owner,,,deny, +API Owner Role Rules,,,Environment,,"update,delete,read",active,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Environment,read,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Environment,create,,,,,api-owner,,,allow, +API Owner Role Rules,,,Environment,,"update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,GatewayConsumer,,"read,update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,GatewayConsumer,create,,,,,api-owner,,,allow, +API Owner Role Rules,,,GatewayGroup,read,,,,,api-owner,,,allow, +API Owner Role Rules,,,GatewayGroup,,"update,delete,create",,,,api-owner,,,allow, +API Owner Role Rules,,,GatewayPlugin,read,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,GatewayRoute,read,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,GatewayService,read,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Group,create,,,,,api-owner,,,allow, +API Owner Role Rules,,,Group,,"update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,Group,read,,,,,api-owner,,,allow, +API Owner Role Rules,,,Legal,read,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Legal,,"update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,MemberRole,create,,,,,api-owner,,,allow, +API Owner Role Rules,,,MemberRole,,"update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,MemberRole,read,,,,,api-owner,,,allow, +API Owner Role Rules,,,Metric,,read,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Namespace,,read,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Namespace,create,,,,,api-owner,,,allow, +API Owner Role Rules,,,Namespace,,"update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,Organization,read,,,,,api-owner,,,allow, +API Owner Role Rules,,,OrganizationUnit,read,,,,,api-owner,,,allow, +API Owner Role Rules,,,Product,update,,namespace,,,,,,deny, +API Owner Role Rules,,,Product,read,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,Product,create,,,,,api-owner,,,allow, +API Owner Role Rules,,,Product,,"update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,ServiceAccess,create,,,,,api-owner,,,allow, +API Owner Role Rules,,,ServiceAccess,,"update,delete",,,,api-owner,,,allow, +API Owner Role Rules,,,ServiceAccess,read,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,,TemporaryIdentity,read,,,,,,,"api-owner,provider-user",allow,filterByTemporaryIdentity +API Owner Role Rules,,,User,read,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,getGatewayConsumerPlugins,,,,,,,api-owner,,,allow, +API Owner Role Rules,,deleteGatewayConsumerPlugin,,,,,,,api-owner,,,allow, +API Owner Role Rules,,updateGatewayConsumerPlugin,,,,,,,api-owner,,,allow, +API Owner Role Rules,,createGatewayConsumerPlugin,,,,,,,api-owner,,,allow, +API Owner Role Rules,,getPermissionTicketsForResource,,,,,,,api-owner,,,allow, +API Owner Role Rules,,allPermissionTickets,,,,,,,api-owner,,,allow, +API Owner Role Rules,,updateConsumerGroupMembership,,,,,,,api-owner,,,allow,filterByPackageNS +API Owner Role Rules,,consumerScopesAndRoles,,,,,,,api-owner,,,allow,filterByPackageNS +API Owner Role Rules,,linkConsumerToNamespace,,,,,,,api-owner,,,allow, +API Owner Role Rules,,updateConsumerRoleAssignment,,,,,,,api-owner,,,allow,filterByPackageNS +API Owner Role Rules,,updateConsumerScopeAssignment,,,,,,,api-owner,,,allow,filterByPackageNS +Portal User Namespaces,,allNamespaces,,,,,,,portal-user,,,allow, +API Owner Role Rules,,createNamespace,,,,,,,idir-user,,,allow, +API Owner Role Rules,,deleteNamespace,,,,,,,api-owner,,,allow, +API Owner Role Rules,,currentNamespace,,,,,,,api-owner,,,allow, +API Owner Role Rules,,grantPermissions,,,,,,,api-owner,,,allow, +API Owner Role Rules,,revokePermissions,,,,,,,api-owner,,,allow, +API Owner Role Rules,,approvePermissions,,,,,,,api-owner,,,allow, +API Owner Role Rules,,getResourceSet,,,,,,,api-owner,,,allow, +API Owner Role Rules,,allResourceSets,,,,,,,api-owner,,,allow, +API Owner Role Rules,,getUmaPoliciesForResource,,,,,,,api-owner,,,allow, +API Owner Role Rules,,createUmaPolicy,,,,,,,api-owner,,,allow, +API Owner Role Rules,,deleteUmaPolicy,,,,,,,api-owner,,,allow, +API Owner Role Rules,,getServiceAccounts,,,,,,,,,"api-owner,provider-user",allow, +API Owner Role Rules,,createServiceAccount,,,,,,,api-owner,,,allow, +API Owner Role Rules,,deleteServiceAccount,,,,,,,api-owner,,,allow, +Developer Role Rules,,myServiceAccesses,,,,,,,portal-user,,,allow,filterByAppOwner +Developer Role Rules,,myApplications,,,,,,,portal-user,,,allow,filterByOwner +Developer Role Rules,,allDiscoverableProducts,,,,,,,portal-user,,,allow,filterByActiveEnvironment +Developer Role Rules,,allDiscoverableContents,,,,,,,portal-user,,,allow, +Developer Role Rules,,DiscoverableProduct,,,,,,,portal-user,,,allow, +Developer Role Rules,,,Product,read,,,,,portal-user,,,allow, +Developer Role Rules,,,Environment,read,,,,,portal-user,,,allow, +Developer Role Rules,,,Dataset,read,,,,,portal-user,,,allow, +Developer Role Rules,,,Organization,read,,,,,portal-user,,,allow, +Developer Role Rules,,,OrganizationUnit,read,,,,,portal-user,,,allow, +Developer Role Rules,,,GatewayService,read,,,,,portal-user,,,allow, +Developer Role Rules,,,GatewayRoute,read,,,,,portal-user,,,allow, +Developer Role Rules,,,GatewayConsumer,read,,,,,portal-user,,,allow, +Developer Role Rules,,,Legal,read,,,,,portal-user,,,allow, +Developer Role Rules,,,TemporaryIdentity,read,,,,,portal-user,,,allow,filterByTemporaryIdentity +Developer Role Rules,,,User,read,,,,,portal-user,,,allow,filterBySelf +Developer Role Rules,,,CredentialIssuer,read,,,,,portal-user,,,allow, +Developer Role Rules,,,AccessRequest,,create,,,,portal-user,,,allow, +Developer Role Rules,,,AccessRequest,,"update,read",,,,portal-user,,,allow,filterByRequestor +Developer Role Rules,,,Application,create,,,,,portal-user,,,allow, +Developer Role Rules,,,Application,,"read,delete,update",,,,portal-user,,,allow,filterByOwner +Developer Role Rules,,,ServiceAccess,delete,,,,,portal-user,,,allow,filterByAppOwner +Developer Role Rules,,,ServiceAccess,read,,,,,portal-user,,,allow,filterByAppOwner +Developer Role Rules - Fields,,,Product,read,,"id,name,dataset,organization,organizationUnit,environments",,,portal-user,,,allow, +Developer Role Rules - Fields,,,Environment,read,,"name,active,flow,services,legal,product,credentialIssuer",,,portal-user,,,allow, +Developer Role Rules - Fields,,,Dataset,read,,,"isInCatalog,extSource,extForeignKey,extRecordHash",,portal-user,,,allow, +Developer Role Rules - Fields,,,Organization,read,,title,,,portal-user,,,allow, +Developer Role Rules - Fields,,,OrganizationUnit,read,,title,,,portal-user,,,allow, +Developer Role Rules - Fields,,,GatewayService,read,,"name,routes",,,portal-user,,,allow, +Developer Role Rules - Fields,,,GatewayRoute,read,,"name,methods,hosts,paths",,,portal-user,,,allow, +Developer Role Rules - Fields,,,Legal,read,,"title,description,link",,,portal-user,,,allow, +Developer Role Rules - Fields,,,TemporaryIdentity,read,,"id,userId,name,email,username",,,portal-user,,,allow, +Developer Role Rules - Fields,,,Content,read,,"slug,title,description",,,portal-user,,,allow, +Developer Role Rules - Fields,,,Application,create,,"name,description",,,portal-user,,,allow, +Developer Role Rules - Fields,,,Application,read,,"appId,name,owner",,,portal-user,,,allow, +Developer Role Rules - Fields,,,User,read,,"name,username,email,legalsAgreed",,,portal-user,,,allow, +Developer Role Rules - Fields,,,ServiceAccess,read,,"name,active,productEnvironment",,,portal-user,,,allow, +Developer Role Rules - Fields,,,CredentialIssuer,read,,"id,name,flow,resourceType",,,portal-user,,,allow, +Developer Role Rules - Fields,,,AccessRequest,create,,"name,controls,requestor,application,productEnvironment",,,portal-user,,,allow, +Developer Role Rules - Fields,,,AccessRequest,,"read,update",credential,,,portal-user,,,allow, +Developer Role Rules - Fields,,,AccessRequest,read,,"name,controls,application,productEnvironment,isIssued",,,portal-user,,,allow, +Developer Role Rules - Fields,,,ServiceAccess,read,,"consumer,application",,,portal-user,,,allow, +API Owner Role - All Fields,,,,,read,,*,,,,"api-owner,provider-user",allow, +API Owner Role - All Fields,,,,,"update,create",,*,,api-owner,,,allow, +Portal User,,DiscoverableProduct,,,,,,,portal-user,,,allow, +Portal User,,myServiceAccesses,,,,,,,portal-user,,,allow,filterByAppOwner +Portal User,,myApplications,,,,,,,portal-user,,,allow,filterByOwner +API Owner Role Rules,,allGatewayServicesByNamespace,,,,,,,,,"api-owner,provider-user",allow,filterByUserNS +API Owner Role Rules,,allProductsByNamespace,,,,,,,,,"api-owner,provider-user",allow,filterByUserNS +API Owner Role Rules,,allAccessRequestsByNamespace,,,,,,,api-owner,,,allow,filterByEnvironmentPackageNS +API Owner Role Rules,,allServiceAccessesByNamespace,,,,,,,api-owner,,,allow,filterByEnvironmentProductNSOrNS +API Owner Role Rules,,allNamespaceServiceAccounts,,,,,,,,,"api-owner,provider-user",allow,filterByUserNS +API Owner Role Rules,,allCredentialIssuersByNamespace,,,,,,,,,"api-owner,provider-user",allow,filterByUserNS +,,getUmaPoliciesByResourceName,,,,,,,api-owner,,,allow, +,,getResourceOwners,,,,,,,api-owner,,,allow, +Portal User or Guest,,allDiscoverableContents,,,,,,,,,"portal-user,guest",allow,filterByNamespaceOrPublic +Portal User or Guest,,allDiscoverableProducts,,,,,,,,,"portal-user,guest",allow,filterByActiveEnvironment +Portal User or Guest,,allProducts,,,,,,,,,"portal-user,guest",allow, +Portal User or Guest,,environments,,,,,,,,,"portal-user,guest",allow, +Portal User or Guest,,allDatasets,,,,,,,,,"portal-user,guest",allow, +Portal User or Guest,,Environment,,,,,,,,,"portal-user,guest",allow, +Portal User or Guest,,,Environment,read,,"name,active,flow,services,legal,product,credentialIssuer",,,,,"portal-user,guest",allow, +Portal User or Guest,,allContents,,,,,,,,,"portal-user,guest",allow, +Portal User or Guest,,DiscoverableProduct,,,,,,,,,"portal-user,guest",allow, +Portal User or Guest,,services,,,,,,,,,"portal-user,guest",allow, +ALL USERS,,mySelf,,,,,,,,,,allow,filterBySelf +ALL USERS,,acceptLegal,,,,,,,,,,allow, ALL USERS,,,User,update,,,,,,,,allow, \ No newline at end of file diff --git a/src/authz/whitelist.json b/src/authz/whitelist.json index ce93460d5..501d8c79a 100644 --- a/src/authz/whitelist.json +++ b/src/authz/whitelist.json @@ -746,5 +746,60 @@ "referer": "http://localhost:4180/manager/requests/60dba098fb76a41bf6a8e74e", "query": "\n query GetAccessRequest($id: ID!, $rid: String!) {\n AccessRequest(where: { id: $id }) {\n id\n name\n isApproved\n isIssued\n controls\n additionalDetails\n createdAt\n requestor {\n name\n username\n email\n }\n application {\n name\n }\n serviceAccess {\n id\n }\n productEnvironment {\n name\n additionalDetailsToRequest\n product {\n name\n }\n credentialIssuer {\n availableScopes\n clientRoles\n }\n }\n }\n\n allActivities(sortBy: createdAt_DESC, where: { refId: $rid }) {\n id\n type\n name\n action\n result\n message\n context\n refId\n namespace\n extRefId\n createdAt\n actor {\n name\n username\n }\n }\n }\n", "added": "2021-06-29T22:48:41.365Z" + }, + "78b1987847e9566263124c3b003f06bd": { + "referer": "http://localhost:4180/manager/consumers/60b53dbaa6802a80a0ff27dd", + "query": "\n mutation ToggleConsumerScopes(\n $prodEnvId: ID!\n $consumerUsername: String!\n $scopeName: String!\n $grant: Boolean!\n ) {\n updateConsumerScopeAssignment(\n prodEnvId: $prodEnvId\n consumerUsername: $consumerUsername\n scopeName: $scopeName\n grant: $grant\n )\n }\n", + "added": "2021-07-02T23:59:23.643Z" + }, + "7238409bbbc0954b2c00cd03a8162054": { + "referer": "http://localhost:4180/manager/consumers/60b53dbaa6802a80a0ff27dd", + "query": "\n query GetConsumer($id: ID!) {\n getGatewayConsumerPlugins(id: $id) {\n id\n username\n aclGroups\n customId\n extForeignKey\n namespace\n plugins {\n id\n name\n extForeignKey\n config\n service {\n id\n name\n }\n route {\n id\n name\n }\n }\n tags\n createdAt\n }\n\n allProductsByNamespace {\n id\n name\n environments {\n id\n appId\n name\n active\n flow\n credentialIssuer {\n id\n availableScopes\n clientRoles\n }\n services {\n name\n routes {\n name\n }\n }\n }\n }\n }\n", + "added": "2021-07-03T00:43:58.265Z" + }, + "1aa9f1d913d81a275515d46091053bec": { + "referer": "http://localhost:4180/manager/consumers/60b53dbaa6802a80a0ff27dd", + "query": "\n query GetConsumer($id: ID!) {\n getGatewayConsumerPlugins(id: $id) {\n id\n username\n aclGroups\n customId\n extForeignKey\n namespace\n plugins {\n id\n name\n extForeignKey\n config\n service {\n id\n name\n }\n route {\n id\n name\n }\n }\n tags\n createdAt\n }\n\n allServiceAccesses(where: { consumer: { id: $id } }) {\n application {\n name\n owner {\n username\n }\n }\n }\n allProductsByNamespace {\n id\n name\n environments {\n id\n appId\n name\n active\n flow\n credentialIssuer {\n id\n availableScopes\n clientRoles\n }\n services {\n name\n routes {\n name\n }\n }\n }\n }\n }\n", + "added": "2021-07-03T01:43:27.940Z" + }, + "13758b33b8f2db459bfb37fd789e5309": { + "referer": "http://localhost:4180/manager/consumers/60b5bff29dc26943fb23d4b6", + "query": "\n query GetConsumer($id: ID!) {\n getGatewayConsumerPlugins(id: $id) {\n id\n username\n aclGroups\n customId\n extForeignKey\n namespace\n plugins {\n id\n name\n extForeignKey\n config\n service {\n id\n name\n }\n route {\n id\n name\n }\n }\n tags\n createdAt\n }\n\n allServiceAccesses(where: { consumer: { id: $id } }) {\n name\n consumerType\n application {\n appId\n name\n owner {\n username\n }\n }\n }\n allProductsByNamespace {\n id\n name\n environments {\n id\n appId\n name\n active\n flow\n credentialIssuer {\n id\n availableScopes\n clientRoles\n }\n services {\n name\n routes {\n name\n }\n }\n }\n }\n }\n", + "added": "2021-07-03T01:46:50.029Z" + }, + "0d9d51ac4ff398172f53dac66b86e481": { + "referer": "http://localhost:4180/manager/consumers/60ad8e1044cf9a36ef674085", + "query": "\n query GetConsumer($id: ID!) {\n getGatewayConsumerPlugins(id: $id) {\n id\n username\n aclGroups\n customId\n extForeignKey\n namespace\n plugins {\n id\n name\n extForeignKey\n config\n service {\n id\n name\n }\n route {\n id\n name\n }\n }\n tags\n createdAt\n }\n\n allServiceAccesses(where: { consumer: { id: $id } }) {\n name\n consumerType\n application {\n appId\n name\n owner {\n name\n username\n email\n }\n }\n }\n allProductsByNamespace {\n id\n name\n environments {\n id\n appId\n name\n active\n flow\n credentialIssuer {\n id\n availableScopes\n clientRoles\n }\n services {\n name\n routes {\n name\n }\n }\n }\n }\n }\n", + "added": "2021-07-03T01:47:50.661Z" + }, + "b35cd8088be328bd3528db32a3ba97cf": { + "referer": "http://localhost:4180/manager/consumers/60ad8e1044cf9a36ef674085", + "query": "\n query GetConsumer($id: ID!) {\n getGatewayConsumerPlugins(id: $id) {\n id\n username\n aclGroups\n customId\n extForeignKey\n namespace\n plugins {\n id\n name\n extForeignKey\n config\n service {\n id\n name\n }\n route {\n id\n name\n }\n }\n tags\n createdAt\n }\n\n allServiceAccesses(where: { consumer: { id: $id } }) {\n name\n consumerType\n application {\n appId\n name\n owner {\n name\n username\n email\n }\n }\n }\n\n allProductsByNamespace {\n id\n name\n environments {\n id\n appId\n name\n active\n flow\n credentialIssuer {\n id\n availableScopes\n clientRoles\n }\n services {\n name\n routes {\n name\n }\n }\n }\n }\n }\n", + "added": "2021-07-03T01:47:59.254Z" + }, + "b2e2c4b6ce7586668c34d3226ae6c5da": { + "referer": "http://localhost:4180/manager/consumers/60b7e9154b74934a7b6ad72a", + "query": "\n mutation updateGatewayConsumerPlugin($id: ID!, $controls: String!) {\n updateGatewayConsumerPlugin(id: $id, plugin: $controls) {\n id\n }\n }\n", + "added": "2021-07-03T03:10:14.327Z" + }, + "7073ccad4a36f9d6b1fae48ad9e00f72": { + "referer": "http://localhost:4180/manager/consumers/60b7e9154b74934a7b6ad72a", + "query": "\n mutation updateGatewayConsumerPlugin(\n $id: ID!\n $pluginExtForeignKey: String!\n $controls: String!\n ) {\n updateGatewayConsumerPlugin(\n id: $id\n pluginExtForeignKey: $pluginExtForeignKey\n plugin: $controls\n ) {\n id\n }\n }\n", + "added": "2021-07-03T03:23:01.128Z" + }, + "a2750969742e7ffa0880f452f82ee58b": { + "referer": "http://localhost:4180/manager/namespace-access", + "query": "\n mutation GrantUserAccess($prodEnvId: ID!, $data: UMAPermissionTicketInput!) {\n grantPermissions(prodEnvId: $prodEnvId, data: $data) {\n id\n }\n }\n", + "added": "2021-07-05T23:29:33.635Z" + }, + "d78565ea5ddbf2b4c6f47b7654cb52c4": { + "referer": "http://localhost:4180/manager/namespace-access", + "query": "\n mutation GrantSAAccess(\n $prodEnvId: ID!\n $resourceId: String!\n $data: UMAPolicyInput!\n ) {\n createUmaPolicy(\n prodEnvId: $prodEnvId\n resourceId: $resourceId\n data: $data\n ) {\n id\n }\n }\n", + "added": "2021-07-06T02:58:10.429Z" + }, + "990f9d0261b747a2d571b0898a9680a8": { + "referer": "http://localhost:4180/manager/namespace-access", + "query": "\n mutation RevokeSAAccess(\n $prodEnvId: ID!\n $resourceId: String!\n $policyId: String!\n ) {\n deleteUmaPolicy(\n prodEnvId: $prodEnvId\n resourceId: $resourceId\n policyId: $policyId\n )\n }\n", + "added": "2021-07-06T02:58:35.159Z" } } \ No newline at end of file diff --git a/src/batch/data-rules.ts b/src/batch/data-rules.ts index baefa3906..87c4e91ba 100644 --- a/src/batch/data-rules.ts +++ b/src/batch/data-rules.ts @@ -310,7 +310,7 @@ export const metadata = { Product: { query: 'allProducts', refKey: 'appId', - sync: ['name'], + sync: ['name', 'namespace'], transformations: { dataset: { name: 'connectOne', list: 'allDatasets', refKey: 'name' }, environments: { diff --git a/src/lists/GatewayConsumer.js b/src/lists/GatewayConsumer.js index 6fd3e0c9b..47d73a0d5 100644 --- a/src/lists/GatewayConsumer.js +++ b/src/lists/GatewayConsumer.js @@ -1,33 +1,36 @@ -const { Text, Checkbox, Relationship } = require('@keystonejs/fields') -const { Markdown } = require('@keystonejs/fields-markdown') +const { Text, Checkbox, Relationship } = require('@keystonejs/fields'); +const { Markdown } = require('@keystonejs/fields-markdown'); -const { externallySourced } = require('../components/ExternalSource') +const { externallySourced } = require('../components/ExternalSource'); -const { byTracking, atTracking } = require('@keystonejs/list-plugins') +const { byTracking, atTracking } = require('@keystonejs/list-plugins'); -const { EnforcementPoint } = require('../authz/enforcement') +const { EnforcementPoint } = require('../authz/enforcement'); -const { lookupConsumerPlugins, lookupKongConsumerId } = require('../services/keystone') +const { + lookupConsumerPlugins, + lookupKongConsumerId, +} = require('../services/keystone'); -const { KongConsumerService } = require('../services/kong') -const { FeederService } = require('../services/feeder') +const { KongConsumerService } = require('../services/kong'); +const { FeederService } = require('../services/feeder'); module.exports = { fields: { username: { - type: Text, - isRequired: true, - isUnique: true, - adminConfig: { - isReadOnly: true - } + type: Text, + isRequired: true, + isUnique: true, + adminConfig: { + isReadOnly: true, + }, }, customId: { - type: Text, - isRequired: false, - adminConfig: { - isReadOnly: true - } + type: Text, + isRequired: false, + adminConfig: { + isReadOnly: true, + }, }, // kongConsumerId: { // type: Text, @@ -37,103 +40,125 @@ module.exports = { // } // }, aclGroups: { - type: Text, - isRequired: false, - adminConfig: { - isReadOnly: true - } + type: Text, + isRequired: false, + adminConfig: { + isReadOnly: true, + }, }, namespace: { - type: Text, - isRequired: false, - adminConfig: { - isReadOnly: true - } + type: Text, + isRequired: false, + adminConfig: { + isReadOnly: true, + }, }, tags: { - type: Text, - isRequired: false, - adminConfig: { - isReadOnly: true - } + type: Text, + isRequired: false, + adminConfig: { + isReadOnly: true, + }, }, - plugins: { type: Relationship, ref: 'GatewayPlugin', many: true } + plugins: { type: Relationship, ref: 'GatewayPlugin', many: true }, }, access: EnforcementPoint, - plugins: [ - externallySourced(), - atTracking() - ], + plugins: [externallySourced(), atTracking()], extensions: [ - (keystone) => { - keystone.extendGraphQLSchema({ - queries: [ - { - schema: 'getGatewayConsumerPlugins(id: ID!): GatewayConsumer', - resolver: async (item, args, context, info, { query, access }) => { - const noauthContext = keystone.createContext({ skipAccessControl: true }) - - return await lookupConsumerPlugins (noauthContext, args.id ) - }, - access: EnforcementPoint, - }, - ], - mutations: [ - { - schema: 'createGatewayConsumerPlugin(id: ID!, plugin: String!): GatewayConsumer', - resolver: async (item, args, context, info, { query, access }) => { - const noauthContext = keystone.createContext({ skipAccessControl: true }) - - const kongApi = new KongConsumerService(process.env.KONG_URL) - const feederApi = new FeederService(process.env.FEEDER_URL) - - const kongConsumerPK = await lookupKongConsumerId (context, args.id) - - const result = await kongApi.addPluginToConsumer (kongConsumerPK, JSON.parse(args.plugin) ) - - await feederApi.forceSync('kong', 'consumer', kongConsumerPK) - return result - }, - access: EnforcementPoint, - }, - { - schema: 'updateGatewayConsumerPlugin(id: ID!, pluginExtForeignKey: String!, plugin: String!): GatewayConsumer', - resolver: async (item, args, context, info, { query, access }) => { - const noauthContext = keystone.createContext({ skipAccessControl: true }) - - const kongApi = new KongConsumerService(process.env.KONG_URL) - const feederApi = new FeederService(process.env.FEEDER_URL) - - const kongConsumerPK = await lookupKongConsumerId (context, args.id) - - const result = await kongApi.updateConsumerPlugin (kongConsumerPK, args.pluginExtForeignKey, JSON.parse(args.plugin) ) - - await feederApi.forceSync('kong', 'consumer', kongConsumerPK) - return result - }, - access: EnforcementPoint, - }, - { - schema: 'deleteGatewayConsumerPlugin(id: ID!, pluginExtForeignKey: String!): GatewayConsumer', - resolver: async (item, args, context, info, { query, access }) => { - const noauthContext = keystone.createContext({ skipAccessControl: true }) - - const kongApi = new KongConsumerService(process.env.KONG_URL) - const feederApi = new FeederService(process.env.FEEDER_URL) - - const kongConsumerPK = await lookupKongConsumerId (context, args.id) - - const result = await kongApi.deleteConsumerPlugin (kongConsumerPK, args.pluginExtForeignKey ) - - await feederApi.forceSync('kong', 'consumer', kongConsumerPK) - return result - }, - access: EnforcementPoint, - } - ] - }); - } - ] - -} + (keystone) => { + keystone.extendGraphQLSchema({ + queries: [ + { + schema: 'getGatewayConsumerPlugins(id: ID!): GatewayConsumer', + resolver: async (item, args, context, info, { query, access }) => { + const noauthContext = keystone.createContext({ + skipAccessControl: true, + }); + + return await lookupConsumerPlugins(noauthContext, args.id); + }, + access: EnforcementPoint, + }, + ], + mutations: [ + { + schema: + 'createGatewayConsumerPlugin(id: ID!, plugin: String!): GatewayConsumer', + resolver: async (item, args, context, info, { query, access }) => { + const kongApi = new KongConsumerService(process.env.KONG_URL); + const feederApi = new FeederService(process.env.FEEDER_URL); + + const kongConsumerPK = await lookupKongConsumerId( + context, + args.id + ); + + const result = await kongApi.addPluginToConsumer( + kongConsumerPK, + JSON.parse(args.plugin) + ); + + await feederApi.forceSync('kong', 'consumer', kongConsumerPK); + return result; + }, + access: EnforcementPoint, + }, + { + schema: + 'updateGatewayConsumerPlugin(id: ID!, pluginExtForeignKey: String!, plugin: String!): GatewayConsumer', + resolver: async (item, args, context, info, { query, access }) => { + const noauthContext = keystone.createContext({ + skipAccessControl: true, + }); + + const kongApi = new KongConsumerService(process.env.KONG_URL); + const feederApi = new FeederService(process.env.FEEDER_URL); + + const kongConsumerPK = await lookupKongConsumerId( + context, + args.id + ); + + const result = await kongApi.updateConsumerPlugin( + kongConsumerPK, + args.pluginExtForeignKey, + JSON.parse(args.plugin) + ); + + await feederApi.forceSync('kong', 'consumer', kongConsumerPK); + return result; + }, + access: EnforcementPoint, + }, + { + schema: + 'deleteGatewayConsumerPlugin(id: ID!, pluginExtForeignKey: String!): GatewayConsumer', + resolver: async (item, args, context, info, { query, access }) => { + const noauthContext = keystone.createContext({ + skipAccessControl: true, + }); + + const kongApi = new KongConsumerService(process.env.KONG_URL); + const feederApi = new FeederService(process.env.FEEDER_URL); + + const kongConsumerPK = await lookupKongConsumerId( + context, + args.id + ); + + const result = await kongApi.deleteConsumerPlugin( + kongConsumerPK, + args.pluginExtForeignKey + ); + + await feederApi.forceSync('kong', 'consumer', kongConsumerPK); + return result; + }, + access: EnforcementPoint, + }, + ], + }); + }, + ], +}; diff --git a/src/lists/extensions/Common.ts b/src/lists/extensions/Common.ts index 0f0dbf899..4341be54e 100644 --- a/src/lists/extensions/Common.ts +++ b/src/lists/extensions/Common.ts @@ -7,6 +7,7 @@ import { import { IssuerEnvironmentConfig, getIssuerEnvironmentConfig, + checkIssuerEnvironmentConfig, } from '../../services/workflow/types'; import { UMAPermissionService, @@ -60,11 +61,15 @@ export async function getEnvironmentContext( access ).where ); - const issuerEnvConfig: IssuerEnvironmentConfig = getIssuerEnvironmentConfig( + const issuerEnvConfig: IssuerEnvironmentConfig = checkIssuerEnvironmentConfig( prodEnv.credentialIssuer, prodEnv.name ); + if (issuerEnvConfig == null) { + return null; + } + const openid = await getOpenidFromIssuer(issuerEnvConfig.issuerUrl); const subjectToken = context.req.headers['x-forwarded-access-token']; const subjectUuid = context.req.user.sub; diff --git a/src/lists/extensions/ConsumerScopesAndRoles.ts b/src/lists/extensions/ConsumerScopesAndRoles.ts index 658536cd1..ebdf92336 100644 --- a/src/lists/extensions/ConsumerScopesAndRoles.ts +++ b/src/lists/extensions/ConsumerScopesAndRoles.ts @@ -8,6 +8,7 @@ import { KongConsumerService } from '../../services/kong'; import { KeycloakUserService, KeycloakClientService, + KeycloakClientRegistrationService, } from '../../services/keycloak'; import { EnvironmentWhereInput } from '@/services/keystone/types'; import { mergeWhereClause } from '@keystonejs/utils'; @@ -72,6 +73,19 @@ module.exports = { args.prodEnvId, access ); + if (envCtx == null) { + logger.warn( + 'Credential Issuer did not have an environment for prodenv %s', + args.prodEnvId + ); + return { + id: '', + consumerType: '', + defaultScopes: [], + optionalScopes: [], + clientRoles: [], + } as any; + } try { const kcClientService = new KeycloakClientService( envCtx.issuerEnvConfig.issuerUrl @@ -107,10 +121,14 @@ module.exports = { userId, client.id ); + const defaultScopes = await kcClientService.listDefaultScopes( + consumerClient.id + ); + return { id: userId, consumerType: 'client', - defaultScopes: [], + defaultScopes: defaultScopes.map((v: any) => v.name), optionalScopes: [], clientRoles: userRoles.map((r: any) => r.name), } as any; @@ -259,6 +277,89 @@ module.exports = { }, access: EnforcementPoint, }, + { + schema: + 'updateConsumerScopeAssignment( prodEnvId: ID!, consumerUsername: String!, scopeName: String!, grant: Boolean! ): Boolean', + resolver: async ( + item: any, + args: any, + context: any, + info: any, + { query, access }: any + ) => { + const envCtx = await getEnvironmentContext( + context, + args.prodEnvId, + access + ); + try { + const kcClientService = new KeycloakClientService( + envCtx.issuerEnvConfig.issuerUrl + ); + const kcClientRegService = new KeycloakClientRegistrationService( + envCtx.issuerEnvConfig.issuerUrl, + null + ); + await kcClientService.login( + envCtx.issuerEnvConfig.clientId, + envCtx.issuerEnvConfig.clientSecret + ); + await kcClientRegService.login( + envCtx.issuerEnvConfig.clientId, + envCtx.issuerEnvConfig.clientSecret + ); + + const client = await kcClientService.findByClientId( + envCtx.issuerEnvConfig.clientId + ); + + const availableScopes = await kcClientService.listDefaultScopes( + client.id + ); + + const selectedScope = availableScopes + .filter((r: any) => r.name === args.scopeName) + .map((r: any) => r.id); + + assert.strictEqual( + selectedScope.length, + 1, + 'Scope not found in IdP' + ); + + logger.debug( + '[updateConsumerScopeAssignment] selected %j', + selectedScope + ); + + const isClient = await kcClientService.isClient( + args.consumerUsername + ); + + assert.strictEqual( + isClient, + true, + 'Only clients support scopes' + ); + + await kcClientRegService.syncClientScopes( + args.consumerUsername, + client.id, + args.grant ? selectedScope : [], + args.grant ? [] : selectedScope + ); + } catch (err) { + logger.error( + '[updateConsumerScopeAssignment] Failed to update %s', + err + ); + throw err; + } + + return args.grant; + }, + access: EnforcementPoint, + }, ], }); }, diff --git a/src/lists/extensions/Namespace.ts b/src/lists/extensions/Namespace.ts index 79b15e12d..3c024e3fc 100644 --- a/src/lists/extensions/Namespace.ts +++ b/src/lists/extensions/Namespace.ts @@ -90,7 +90,7 @@ module.exports = { '[currentNamespace] NOT FOUND! %j', context.req.user ); - return {}; + return null; } else { return matched[0]; } diff --git a/src/nextapp/components/consumer-acl/consumer-acl.tsx b/src/nextapp/components/consumer-acl/consumer-acl.tsx index d6937a4c3..ae132dc2c 100644 --- a/src/nextapp/components/consumer-acl/consumer-acl.tsx +++ b/src/nextapp/components/consumer-acl/consumer-acl.tsx @@ -1,116 +1,40 @@ import * as React from 'react'; import { Product, Environment } from '@/shared/types/query.types'; -import { Box, Divider, Text, Switch, useToast } from '@chakra-ui/react'; -import { gql } from 'graphql-request'; -import { QueryKey, useQueryClient } from 'react-query'; -import { useApiMutation } from '@/shared/services/api'; +import { Box, Divider, Heading, Switch } from '@chakra-ui/react'; interface ConsumerACLProps { - queryKey: any[]; - consumerId: string; aclGroups: string[]; products: Product[]; } -const ConsumerACL: React.FC = ({ - queryKey, - consumerId, - aclGroups, - products, -}) => { - const client = useQueryClient(); - const grantMutation = useApiMutation(mutation); - const toast = useToast(); - const handleGrantToggle = React.useCallback( - (prodEnvId: string, group: string) => async ( - event: React.ChangeEvent - ) => { - event.preventDefault(); - event.stopPropagation(); - - const grant = event.target.checked; - - try { - await grantMutation.mutateAsync({ - prodEnvId, - group, - consumerId, - grant, - }); - client.invalidateQueries(queryKey); - toast({ - title: 'ACL Updated', - status: 'success', - }); - } catch (err) { - toast({ - title: 'ACL update failed', - description: err?.message, - status: 'error', - }); - } - }, - [client, queryKey, grantMutation, toast] - ); - +const ConsumerACL: React.FC = ({ aclGroups, products }) => { const productGroups = [].concat .apply( [], products.map((product) => product.environments) ) .map((env: Environment) => env.appId); - return ( - - {products.map((product: Product) => { - return product.environments.map((env: Environment) => ( - - {' '} - {product.name}{' '} - - {env.name} - - - )); - })} + const readonlyGroups = aclGroups.filter( + (group: string) => !productGroups.includes(group) + ); + return readonlyGroups.length > 0 ? ( + <> + + Legacy ACL Groups + - {aclGroups - .filter((group: string) => !productGroups.includes(group)) - .map((group: string) => ( + + + {readonlyGroups.map((group: string) => ( {group} ))} - + + + ) : ( + <> ); }; export default ConsumerACL; - -const mutation = gql` - mutation ToggleConsumerACLMembership( - $prodEnvId: ID! - $consumerId: ID! - $group: String! - $grant: Boolean! - ) { - updateConsumerGroupMembership( - prodEnvId: $prodEnvId - consumerId: $consumerId - group: $group - grant: $grant - ) - } -`; diff --git a/src/nextapp/components/consumer-authz/consumer-authz.tsx b/src/nextapp/components/consumer-authz/consumer-authz.tsx new file mode 100644 index 000000000..225d4a31f --- /dev/null +++ b/src/nextapp/components/consumer-authz/consumer-authz.tsx @@ -0,0 +1,159 @@ +import * as React from 'react'; +import { Product, Environment } from '@/shared/types/query.types'; +import { + Box, + Divider, + Icon, + Heading, + Text, + Switch, + useToast, +} from '@chakra-ui/react'; +import { gql } from 'graphql-request'; +import { QueryKey, useQueryClient } from 'react-query'; +import { useApiMutation } from '@/shared/services/api'; +import { FaPlusCircle, FaFolder, FaFolderOpen } from 'react-icons/fa'; + +import ACLComponent from './types/acl'; +import RolesComponent from './types/roles'; +import ScopesComponent from './types/scopes'; + +interface ConsumerACLProps { + queryKey?: any[]; + consumerId: string; + consumerUsername: string; + consumerAclGroups: string[]; + aclGroups?: string[]; + products: Product[]; +} + +const ConsumerAuthz: React.FC = ({ + queryKey, + products, + consumerId, + consumerUsername, + consumerAclGroups, +}) => { + return ( + <> + + Authorization + + + + {products + .filter((p) => p.environments.length > 0) + .filter( + (p) => p.environments.filter((e) => e.flow != 'public').length > 0 + ) + .map((d) => ( + + + + + 0 ? FaFolder : FaFolderOpen} + color={ + d.environments.length > 0 ? 'bc-blue-alt' : 'gray.200' + } + mr={4} + boxSize="1.5rem" + /> + + {d.name} + + + + + + {d.environments.map((e, index: number, arr: any) => ( + + + + + {e.name} + + + + {e.flow === 'client-credentials' && ( + + + + )} + {e.flow === 'client-credentials' && ( + + + + )} + {(e.flow === 'kong-api-key-acl' || + e.flow == 'kong-acl-only') && ( + + + + )} + + ))} + + + + + ))} + + + + ); +}; + +export default ConsumerAuthz; diff --git a/src/nextapp/components/consumer-authz/index.ts b/src/nextapp/components/consumer-authz/index.ts new file mode 100644 index 000000000..cefe3e74c --- /dev/null +++ b/src/nextapp/components/consumer-authz/index.ts @@ -0,0 +1 @@ +export { default } from './consumer-authz'; diff --git a/src/nextapp/components/consumer-authz/types/acl.tsx b/src/nextapp/components/consumer-authz/types/acl.tsx new file mode 100644 index 000000000..6b6eade96 --- /dev/null +++ b/src/nextapp/components/consumer-authz/types/acl.tsx @@ -0,0 +1,84 @@ +import * as React from 'react'; +import { Product, Environment } from '@/shared/types/query.types'; +import { Box, Divider, Text, Switch, useToast } from '@chakra-ui/react'; +import { gql } from 'graphql-request'; +import { QueryKey, useQueryClient } from 'react-query'; +import { useApiMutation } from '@/shared/services/api'; + +interface ConsumerACLProps { + queryKey: any[]; + consumerId: string; + aclGroups: string[]; + env: Environment; +} + +const ConsumerACL: React.FC = ({ + queryKey, + consumerId, + aclGroups, + env, +}) => { + const client = useQueryClient(); + const grantMutation = useApiMutation(mutation); + const toast = useToast(); + const handleGrantToggle = React.useCallback( + (prodEnvId: string, group: string) => async ( + event: React.ChangeEvent + ) => { + event.preventDefault(); + event.stopPropagation(); + + const grant = event.target.checked; + + try { + await grantMutation.mutateAsync({ + prodEnvId, + group, + consumerId, + grant, + }); + client.invalidateQueries(queryKey); + toast({ + title: 'ACL Updated', + status: 'success', + }); + } catch (err) { + toast({ + title: 'ACL update failed', + description: err?.message, + status: 'error', + }); + } + }, + [client, queryKey, grantMutation, toast] + ); + + return ( + + + + ); +}; + +export default ConsumerACL; + +const mutation = gql` + mutation ToggleConsumerACLMembership( + $prodEnvId: ID! + $consumerId: ID! + $group: String! + $grant: Boolean! + ) { + updateConsumerGroupMembership( + prodEnvId: $prodEnvId + consumerId: $consumerId + group: $group + grant: $grant + ) + } +`; diff --git a/src/nextapp/components/consumer-permissions/roles.tsx b/src/nextapp/components/consumer-authz/types/roles.tsx similarity index 96% rename from src/nextapp/components/consumer-permissions/roles.tsx rename to src/nextapp/components/consumer-authz/types/roles.tsx index 016beeb38..0195509ef 100644 --- a/src/nextapp/components/consumer-permissions/roles.tsx +++ b/src/nextapp/components/consumer-authz/types/roles.tsx @@ -43,7 +43,7 @@ const RolesComponent: React.FC = ({ prodEnvId, credentialIssuer, }) => { - const queryKey = ['consumer-scopes-roles', prodEnvId, consumerUsername]; + const queryKey = ['consumer-roles', prodEnvId, consumerUsername]; const variables = { prodEnvId, consumerUsername }; const { data, isFetching, isLoading, isSuccess } = useApi( queryKey, @@ -103,6 +103,9 @@ const RolesComponent: React.FC = ({ if (data == null) { return Error; } + if (data.consumerScopesAndRoles.id == '') { + return <>; + } const clientRoles = credentialIssuer.clientRoles ? JSON.parse(credentialIssuer.clientRoles) : []; diff --git a/src/nextapp/components/consumer-authz/types/scopes.tsx b/src/nextapp/components/consumer-authz/types/scopes.tsx new file mode 100644 index 000000000..cf0a2e24b --- /dev/null +++ b/src/nextapp/components/consumer-authz/types/scopes.tsx @@ -0,0 +1,163 @@ +import * as React from 'react'; +import { useApi } from '@/shared/services/api'; +import { gql } from 'graphql-request'; +import { CredentialIssuer } from '@/shared/types/query.types'; +import { + Button, + Checkbox, + CheckboxGroup, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalOverlay, + ModalHeader, + Input, + ButtonGroup, + FormControl, + FormLabel, + Icon, + useDisclosure, + VStack, + Progress, + useToast, + Box, + Text, + WrapItem, + Wrap, + useBoolean, +} from '@chakra-ui/react'; +import { QueryKey, useQueryClient } from 'react-query'; +import { useApiMutation } from '@/shared/services/api'; + +interface ScopesProps { + prodEnvId: string; + consumerId: string; + consumerUsername: string; + credentialIssuer: CredentialIssuer; +} + +const ScopesComponent: React.FC = ({ + consumerId, + consumerUsername, + prodEnvId, + credentialIssuer, +}) => { + const queryKey = ['consumer-scopes', prodEnvId, consumerUsername]; + const variables = { prodEnvId, consumerUsername }; + const { data, isFetching, isLoading, isSuccess } = useApi( + queryKey, + { + query, + variables, + }, + { + retry: false, + suspense: false, + } + ); + + const [busy, setBusy] = useBoolean(false); + + const client = useQueryClient(); + const grantMutation = useApiMutation(mutation); + const toast = useToast(); + const handleGrantToggle = React.useCallback( + (scopeName: string) => async ( + event: React.ChangeEvent + ) => { + event.preventDefault(); + event.stopPropagation(); + + const grant = event.target.checked; + + try { + setBusy.on(); + await grantMutation.mutateAsync({ + prodEnvId, + scopeName, + consumerUsername, + grant, + }); + toast({ + title: `Scope ${scopeName} ${grant ? 'assigned' : 'removed'}`, + status: 'success', + }); + setBusy.off(); + client.invalidateQueries(queryKey); + } catch (err) { + toast({ + title: 'Scope update failed', + description: err?.message, + status: 'error', + }); + setBusy.off(); + } + }, + [client, queryKey, grantMutation, toast, busy] + ); + + if (isLoading || isFetching || busy) { + return ; + } + if (data == null) { + return Error; + } + if (data.consumerScopesAndRoles.id == '') { + return <>; + } + const clientScopes = credentialIssuer.availableScopes + ? JSON.parse(credentialIssuer.availableScopes) + : []; + + return ( + + + {clientScopes.map((scope) => ( + + + {scope} + + + ))} + + + ); +}; + +export default ScopesComponent; + +const mutation = gql` + mutation ToggleConsumerScopes( + $prodEnvId: ID! + $consumerUsername: String! + $scopeName: String! + $grant: Boolean! + ) { + updateConsumerScopeAssignment( + prodEnvId: $prodEnvId + consumerUsername: $consumerUsername + scopeName: $scopeName + grant: $grant + ) + } +`; + +const query = gql` + query GetConsumerScopesAndRoles($prodEnvId: ID!, $consumerUsername: ID!) { + consumerScopesAndRoles( + prodEnvId: $prodEnvId + consumerUsername: $consumerUsername + ) { + id + consumerType + defaultScopes + optionalScopes + clientRoles + } + } +`; diff --git a/src/nextapp/components/consumer-permissions/consumer-permissions.tsx b/src/nextapp/components/consumer-permissions/consumer-permissions.tsx deleted file mode 100644 index e6df577f4..000000000 --- a/src/nextapp/components/consumer-permissions/consumer-permissions.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import * as React from 'react'; -import { Product, Environment } from '@/shared/types/query.types'; -import { - FormControl, - FormLabel, - Box, - Checkbox, - CheckboxGroup, - Divider, - Grid, - Switch, - Text, - Wrap, - WrapItem, - useToast, -} from '@chakra-ui/react'; -import { gql } from 'graphql-request'; -import { QueryKey, useQueryClient } from 'react-query'; -import { useApiMutation } from '@/shared/services/api'; -import RolesComponent from './roles'; - -interface ConsumerACLProps { - queryKey: any[]; - consumerId: string; - consumerUsername: string; - products: Product[]; -} - -const ConsumerPermissions: React.FC = ({ - queryKey, - consumerId, - consumerUsername, - products, -}) => { - return ( - - - Roles - Scopes - {products.map((product) => { - return product.environments - .filter((env) => env.credentialIssuer != null) - .map((env) => ( - <> - - {product.name}{' '} - - {env.name} - - - - - - )); - })} - - ); -}; - -export default ConsumerPermissions; diff --git a/src/nextapp/components/consumer-permissions/index.ts b/src/nextapp/components/consumer-permissions/index.ts deleted file mode 100644 index 493f53fc9..000000000 --- a/src/nextapp/components/consumer-permissions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './consumer-permissions'; diff --git a/src/nextapp/components/controls-list/controls-list.tsx b/src/nextapp/components/controls-list/controls-list.tsx index 7d37df2c6..c5f9d9ad0 100644 --- a/src/nextapp/components/controls-list/controls-list.tsx +++ b/src/nextapp/components/controls-list/controls-list.tsx @@ -88,20 +88,23 @@ const ControlsList: React.FC = ({ consumerId, data }) => { {d.name === 'ip-restriction' && ( )} {d.name === 'rate-limiting' && ( )} - + @@ -127,21 +130,24 @@ const ControlsList: React.FC = ({ consumerId, data }) => { {d.name === 'ip-restriction' && ( )} {d.name === 'rate-limiting' && ( )} - + diff --git a/src/nextapp/components/controls/ip-restriction.tsx b/src/nextapp/components/controls/ip-restriction.tsx index c0b2dd475..dc999db9a 100644 --- a/src/nextapp/components/controls/ip-restriction.tsx +++ b/src/nextapp/components/controls/ip-restriction.tsx @@ -12,7 +12,7 @@ import { useQueryClient, QueryKey } from 'react-query'; import ControlsDialog from './controls-dialog'; import ControlTypeSelect from './control-type-select'; -import { FULFILL_REQUEST } from './queries'; +import { CREATE_PLUGIN, UPDATE_PLUGIN } from './queries'; type ControlsPayload = { name: string; @@ -42,9 +42,11 @@ const IpRestriction: React.FC = ({ queryKey, }) => { const client = useQueryClient(); - const mutation = useApiMutation<{ id: string; controls: string }>( - FULFILL_REQUEST - ); + const mutation = useApiMutation<{ + id: string; + controls: string; + pluginExtForeignKey?: string; + }>(mode == 'edit' ? UPDATE_PLUGIN : CREATE_PLUGIN); const toast = useToast(); const config = data?.config ? JSON.parse(data.config) @@ -78,6 +80,10 @@ const IpRestriction: React.FC = ({ id, controls: JSON.stringify(controls), }; + if (mode == 'edit') { + payload['pluginExtForeignKey'] = data.extForeignKey; + } + await mutation.mutateAsync(payload); client.invalidateQueries(queryKey); toast({ diff --git a/src/nextapp/components/controls/queries.ts b/src/nextapp/components/controls/queries.ts index cd88df37d..195811f09 100644 --- a/src/nextapp/components/controls/queries.ts +++ b/src/nextapp/components/controls/queries.ts @@ -1,9 +1,25 @@ import { gql } from 'graphql-request'; -export const FULFILL_REQUEST = gql` +export const CREATE_PLUGIN = gql` mutation createGatewayConsumerPlugin($id: ID!, $controls: String!) { createGatewayConsumerPlugin(id: $id, plugin: $controls) { id } } `; + +export const UPDATE_PLUGIN = gql` + mutation updateGatewayConsumerPlugin( + $id: ID! + $pluginExtForeignKey: String! + $controls: String! + ) { + updateGatewayConsumerPlugin( + id: $id + pluginExtForeignKey: $pluginExtForeignKey + plugin: $controls + ) { + id + } + } +`; diff --git a/src/nextapp/components/controls/rate-limiting.tsx b/src/nextapp/components/controls/rate-limiting.tsx index 1c6b0747d..79913bff8 100644 --- a/src/nextapp/components/controls/rate-limiting.tsx +++ b/src/nextapp/components/controls/rate-limiting.tsx @@ -13,7 +13,7 @@ import { useApiMutation } from '@/shared/services/api'; import ControlsDialog from './controls-dialog'; import ControlTypeSelect from './control-type-select'; -import { FULFILL_REQUEST } from './queries'; +import { CREATE_PLUGIN, UPDATE_PLUGIN } from './queries'; type ControlsPayload = { name: string; @@ -49,7 +49,7 @@ const RateLimiting: React.FC = ({ }) => { const client = useQueryClient(); const mutation = useApiMutation<{ id: string; controls: string }>( - FULFILL_REQUEST + mode == 'edit' ? UPDATE_PLUGIN : CREATE_PLUGIN ); const toast = useToast(); const config = data?.config @@ -96,6 +96,10 @@ const RateLimiting: React.FC = ({ id, controls: JSON.stringify(controls), }; + if (mode == 'edit') { + payload['pluginExtForeignKey'] = data.extForeignKey; + } + await mutation.mutateAsync(payload); client.invalidateQueries(queryKey); toast({ diff --git a/src/nextapp/components/discovery-list/discovery-list-item.tsx b/src/nextapp/components/discovery-list/discovery-list-item.tsx index c2bf1b0c8..f135af784 100644 --- a/src/nextapp/components/discovery-list/discovery-list-item.tsx +++ b/src/nextapp/components/discovery-list/discovery-list-item.tsx @@ -45,12 +45,12 @@ const DiscoveryListItem: React.FC = ({ data }) => { {data.dataset ? ( <> - + {data.dataset.title} ) : ( - + {data.name} )} @@ -60,10 +60,10 @@ const DiscoveryListItem: React.FC = ({ data }) => { - {data.organization && ( + {data.dataset?.organization && ( <> - {data.organization.title} - {data.organizationUnit && ( + {data.dataset.organization.title} + {data.dataset.organizationUnit && ( <> = ({ data }) => { mt={1} fontSize="xs" > - {data.organizationUnit.title} + {data.dataset.organizationUnit.title} )} )} - {!data.organization && 'Open Dataset'} + {!data.dataset?.organization && 'Open Dataset'} {data.dataset && ( diff --git a/src/nextapp/components/grant-access-dialog/grant-access-dialog.tsx b/src/nextapp/components/grant-access-dialog/grant-access-dialog.tsx index 337206664..7bd48c94f 100644 --- a/src/nextapp/components/grant-access-dialog/grant-access-dialog.tsx +++ b/src/nextapp/components/grant-access-dialog/grant-access-dialog.tsx @@ -39,7 +39,7 @@ const ShareResourceDialog: React.FC = ({ prodEnvId, resource, resourceId, - queryKey + queryKey, }) => { const client = useQueryClient(); const grant = useApiMutation(mutation); @@ -114,7 +114,7 @@ const ShareResourceDialog: React.FC = ({ Permissions - {resource.resource_scopes.map((r) => ( + {resource?.resource_scopes.map((r) => ( {r.name} @@ -144,10 +144,7 @@ const ShareResourceDialog: React.FC = ({ export default ShareResourceDialog; const mutation = gql` - mutation GrantUserAccess( - $prodEnvId: ID! - $data: UMAPermissionTicketInput! - ) { + mutation GrantUserAccess($prodEnvId: ID!, $data: UMAPermissionTicketInput!) { grantPermissions(prodEnvId: $prodEnvId, data: $data) { id } diff --git a/src/nextapp/components/grant-service-account-dialog/grant-access-dialog.tsx b/src/nextapp/components/grant-service-account-dialog/grant-access-dialog.tsx index 084760b9e..8c74224b8 100644 --- a/src/nextapp/components/grant-service-account-dialog/grant-access-dialog.tsx +++ b/src/nextapp/components/grant-service-account-dialog/grant-access-dialog.tsx @@ -118,7 +118,7 @@ const ShareResourceDialog: React.FC = ({ Permissions - {resource.resource_scopes.map((r) => ( + {resource?.resource_scopes.map((r) => ( {r.name} diff --git a/src/nextapp/components/inline-permissions-list/inline-permissions-list.tsx b/src/nextapp/components/inline-permissions-list/inline-permissions-list.tsx index 504736df0..9454a4dc8 100644 --- a/src/nextapp/components/inline-permissions-list/inline-permissions-list.tsx +++ b/src/nextapp/components/inline-permissions-list/inline-permissions-list.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; -import { HStack, Tag, TagCloseButton } from '@chakra-ui/react'; +import { Tag, TagCloseButton, Wrap, WrapItem } from '@chakra-ui/react'; interface InlinePermissionsListProps { data: { id: string; - scope: string; scopeName: string; }[]; enableRevoke: boolean; @@ -24,14 +23,16 @@ const InlinePermissionsList: React.FC = ({ ); return ( - + {data.map((p) => ( - - {p.scopeName} - {enableRevoke && } - + + + {p.scopeName} + {enableRevoke && } + + ))} - + ); }; diff --git a/src/nextapp/components/namespace-delete/namespace-delete.tsx b/src/nextapp/components/namespace-delete/namespace-delete.tsx index 6ad03694d..0bb44cd85 100644 --- a/src/nextapp/components/namespace-delete/namespace-delete.tsx +++ b/src/nextapp/components/namespace-delete/namespace-delete.tsx @@ -13,7 +13,7 @@ import { } from '@chakra-ui/react'; import { useQueryClient } from 'react-query'; import { gql } from 'graphql-request'; -import { useApiMutation } from '@/shared/services/api'; +import { restApi, useApiMutation } from '@/shared/services/api'; import { useAuth } from '@/shared/services/auth'; import { useRouter } from 'next/router'; @@ -40,6 +40,7 @@ const NamespaceDelete: React.FC = ({ if (user.namespace === name && router) { router.push('/manager'); + await restApi('/admin/switch', { method: 'PUT' }); } toast({ @@ -96,4 +97,3 @@ const mutation = gql` deleteNamespace(namespace: $name) } `; - diff --git a/src/nextapp/components/new-namespace/new-namespace.tsx b/src/nextapp/components/new-namespace/new-namespace.tsx index ef862809a..c1c02fbb7 100644 --- a/src/nextapp/components/new-namespace/new-namespace.tsx +++ b/src/nextapp/components/new-namespace/new-namespace.tsx @@ -16,7 +16,7 @@ import { import { useMutation, useQueryClient } from 'react-query'; import { gql } from 'graphql-request'; import { restApi, useApiMutation } from '@/shared/services/api'; -import type { Mutation } from '@/types/query.types' +import type { Mutation } from '@/types/query.types'; interface NewNamespace { isOpen: boolean; onClose: () => void; @@ -40,12 +40,15 @@ const NewNamespace: React.FC = ({ isOpen, onClose }) => { const name = data.get('name') as string; const json: Mutation = await createMutation.mutateAsync({ name, - }) + }); toast({ title: `Namespace ${json.createNamespace.name} created!`, status: 'success', }); + await restApi(`/admin/switch/${json.createNamespace.id}`, { + method: 'PUT', + }); queryClient.invalidateQueries(); toast({ title: `Switched to ${json.createNamespace.name} namespace`, diff --git a/src/nextapp/components/service-accounts-list/index.ts b/src/nextapp/components/service-accounts-list/index.ts new file mode 100644 index 000000000..1ebcf7658 --- /dev/null +++ b/src/nextapp/components/service-accounts-list/index.ts @@ -0,0 +1 @@ +export { default } from './service-accounts-list'; diff --git a/src/nextapp/components/service-accounts-list/service-accounts-list.tsx b/src/nextapp/components/service-accounts-list/service-accounts-list.tsx new file mode 100644 index 000000000..8e58e8c16 --- /dev/null +++ b/src/nextapp/components/service-accounts-list/service-accounts-list.tsx @@ -0,0 +1,119 @@ +import * as React from 'react'; +import { + Button, + Icon, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableCaption, + useToast, +} from '@chakra-ui/react'; +import { FaMinusCircle } from 'react-icons/fa'; +import { gql } from 'graphql-request'; +import InlinePermissionsList from '@/components/inline-permissions-list'; +import { useApiMutation } from '@/shared/services/api'; +import { QueryKey, useQueryClient } from 'react-query'; +import { UmaPolicy } from '@/shared/types/query.types'; + +interface ServiceAccountsListProps { + data: UmaPolicy[]; + prodEnvId: string; + resourceId: string; + queryKey: QueryKey; +} + +const ServiceAccountsList: React.FC = ({ + data, + prodEnvId, + resourceId, + queryKey, +}) => { + const toast = useToast(); + const client = useQueryClient(); + const revoke = useApiMutation(revokeMutation); + const list = data?.sort((a, b) => a.name.localeCompare(b.name)); + + const handleRevoke = async (policyId: string) => { + try { + await revoke.mutateAsync({ prodEnvId, resourceId, policyId }); + toast({ + title: 'Access Revoked', + status: 'success', + }); + client.invalidateQueries(queryKey); + } catch (err) { + toast({ + title: 'Revoke Access Scope Failed', + description: err?.message, + status: 'error', + }); + } + }; + + return ( + <> + + - + + + + + + + + + {list + ?.filter((p) => p.users == null) + .map((item) => ( + + + + + + ))} + +
SubjectPermissionActions
+ {item.clients != null + ? item.clients.join(',') + : item.users.join(',')} + + ({ id: s, scopeName: s }))} + onRevoke={() => false} + /> + + +
+ + ); +}; + +export default ServiceAccountsList; + +const revokeMutation = gql` + mutation RevokeSAAccess( + $prodEnvId: ID! + $resourceId: String! + $policyId: String! + ) { + deleteUmaPolicy( + prodEnvId: $prodEnvId + resourceId: $resourceId + policyId: $policyId + ) + } +`; diff --git a/src/nextapp/pages/devportal/access/index.tsx b/src/nextapp/pages/devportal/access/index.tsx index f7896af4d..b80960ff9 100644 --- a/src/nextapp/pages/devportal/access/index.tsx +++ b/src/nextapp/pages/devportal/access/index.tsx @@ -43,23 +43,21 @@ const ApiAccessPage: React.FC< API Program Services | API Access - - + - + List of the BC Government Service APIs that you have access to. - + - {data.myServiceAccesses.length == 0 && ( + message="Go to the Directory to find one today!" + title="Not using any APIs yet?" + /> )} diff --git a/src/nextapp/pages/devportal/api-discovery/[id].tsx b/src/nextapp/pages/devportal/api-directory/[id].tsx similarity index 72% rename from src/nextapp/pages/devportal/api-discovery/[id].tsx rename to src/nextapp/pages/devportal/api-directory/[id].tsx index 33c368067..604758743 100644 --- a/src/nextapp/pages/devportal/api-discovery/[id].tsx +++ b/src/nextapp/pages/devportal/api-directory/[id].tsx @@ -28,6 +28,14 @@ import { FaTimesCircle, } from 'react-icons/fa'; import TagsList from '@/components/tags-list'; +import ReactMarkdownWithHtml from 'react-markdown/with-html'; +import gfm from 'remark-gfm'; +import { DocHeader, InternalLink } from '@/components/docs'; + +const renderers = { + link: InternalLink, + heading: DocHeader, +}; type DetailItem = { title: string; @@ -89,7 +97,7 @@ const ApiPage: React.FC< return ( <> - API Program Services | API Discovery + API Services Portal | API Directory } title={ - - {data?.name} - - + data?.dataset?.isInCatalog ? ( + + {data?.name} + + + ) : ( + {data?.name} + ) } > + {data.dataset?.organization && ( + + + Published by the{' '} + + {data.dataset.organization.title} + {' - '} + + {data.dataset.organizationUnit && ( + {data.dataset.organizationUnit.title} + )} + + + )} Licensed under{' '} @@ -132,7 +158,9 @@ const ApiPage: React.FC< - {data.dataset?.notes} + + {data.dataset?.notes} + diff --git a/src/nextapp/pages/devportal/api-discovery/index.tsx b/src/nextapp/pages/devportal/api-directory/index.tsx similarity index 95% rename from src/nextapp/pages/devportal/api-discovery/index.tsx rename to src/nextapp/pages/devportal/api-directory/index.tsx index 2b960f735..104d93066 100644 --- a/src/nextapp/pages/devportal/api-discovery/index.tsx +++ b/src/nextapp/pages/devportal/api-directory/index.tsx @@ -38,10 +38,10 @@ const ApiDiscoveryPage: React.FC< return ( <> - API Program Services | API Discovery + API Services Portal | API Directory - + Find an API and request an API key to get started diff --git a/src/nextapp/pages/devportal/requests/new/[id].tsx b/src/nextapp/pages/devportal/requests/new/[id].tsx index 9d3e59f18..948b76477 100644 --- a/src/nextapp/pages/devportal/requests/new/[id].tsx +++ b/src/nextapp/pages/devportal/requests/new/[id].tsx @@ -34,12 +34,10 @@ import { dehydrate } from 'react-query/hydration'; import { FieldsetBox, RadioGroup } from '@/components/forms'; import { FaBook } from 'react-icons/fa'; import { useRouter } from 'next/router'; -import isString from 'lodash/isString'; +import isNotBlank from '@/shared/isNotBlank'; const queryKey = 'newAccessRequest'; -const isNotBlank = (v: any) => isString(v) && v.length > 0; - export const getServerSideProps: GetServerSideProps = async (context) => { const { id } = context.params; const queryClient = new QueryClient(); diff --git a/src/nextapp/pages/devportal/resources/[id].tsx b/src/nextapp/pages/devportal/resources/[id].tsx index 5f1de171c..fbd65d505 100644 --- a/src/nextapp/pages/devportal/resources/[id].tsx +++ b/src/nextapp/pages/devportal/resources/[id].tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import api, { useApi } from '@/shared/services/api'; -import { Box, Container, Divider, Heading, Text } from '@chakra-ui/react'; +import { Box, Container, Divider, Heading } from '@chakra-ui/react'; import EmptyPane from '@/components/empty-pane'; import GrantAccessDialog from '@/components/grant-access-dialog'; import GrantServiceAccountDialog from '@/components/grant-service-account-dialog'; @@ -15,7 +15,7 @@ import { Query } from '@/shared/types/query.types'; import { dehydrate } from 'react-query/hydration'; import { gql } from 'graphql-request'; -import ServiceAccounts from './service-accounts' +import ServiceAccounts from '@/components/service-accounts-list'; export const getServerSideProps: GetServerSideProps = async (context) => { const { id } = context.params; @@ -50,9 +50,11 @@ const ApiAccessResourcePage: React.FC< > = ({ queryKey, variables }) => { const { data } = useApi(queryKey, { query, variables }, { suspense: false }); const { prodEnvId, resourceId } = variables; - const requests = data.getPermissionTicketsForResource?.filter((p) => !p.granted); + const requests = data.getPermissionTicketsForResource?.filter( + (p) => !p.granted + ); - const resource = data.getResourceSet + const resource = data.getResourceSet; return ( <> @@ -73,7 +75,10 @@ const ApiAccessResourcePage: React.FC< } breadcrumb={[ { href: '/devportal/access', text: 'API Access' }, - { href: '/devportal/access/' + data?.Environment?.product.id, text: `${data?.Environment?.product.name} Resources` }, + { + href: '/devportal/access/' + data?.Environment?.product.id, + text: `${data?.Environment?.product.name} Resources`, + }, ]} title={`${resource.type} ${resource.name}`} /> @@ -101,12 +106,13 @@ const ApiAccessResourcePage: React.FC< )} p.granted)} + data={data?.getPermissionTicketsForResource.filter( + (p) => p.granted + )} resourceId={resourceId} prodEnvId={prodEnvId} queryKey={queryKey} /> - @@ -125,7 +131,12 @@ const ApiAccessResourcePage: React.FC< /> - +
@@ -136,7 +147,10 @@ export default ApiAccessResourcePage; const query = gql` query GetPermissions($resourceId: String!, $prodEnvId: ID!) { - getPermissionTicketsForResource(prodEnvId: $prodEnvId, resourceId: $resourceId) { + getPermissionTicketsForResource( + prodEnvId: $prodEnvId + resourceId: $resourceId + ) { id owner ownerName @@ -170,7 +184,7 @@ const query = gql` name } } - + Environment(where: { id: $prodEnvId }) { name product { diff --git a/src/nextapp/pages/devportal/resources/service-accounts.tsx b/src/nextapp/pages/devportal/resources/service-accounts.tsx deleted file mode 100644 index 031394e84..000000000 --- a/src/nextapp/pages/devportal/resources/service-accounts.tsx +++ /dev/null @@ -1,91 +0,0 @@ - -import { Button, Icon, Table, Thead, Tbody, Tr, Th, Td, TableCaption, useToast } from "@chakra-ui/react" -import { gql } from 'graphql-request'; - -import InlinePermissionsList from '@/components/inline-permissions-list'; -import { useApiMutation } from '@/shared/services/api'; -import { QueryKey, useQueryClient } from 'react-query'; -import { FaCheck, FaMinusCircle } from 'react-icons/fa'; - -interface RevokeVariables { - prodEnvId: string - resourceId: string - policyId: string - } - -function List({ prodEnvId, resourceId, data, queryKey }) { - const list = data?.sort((a,b) => a.name.localeCompare(b.name)) - - const toast = useToast(); - const client = useQueryClient(); - const revoke = useApiMutation(revokeMutation); - - const handleRevoke = async (policyId: string) => { - try { - await revoke.mutateAsync({ prodEnvId, resourceId, policyId }); - toast({ - title: 'Access Revoked', - status: 'success', - }); - client.invalidateQueries(queryKey); - } catch (err) { - toast({ - title: 'Revoke Access Scope Failed', - description: err?.message, - status: 'error', - }); - } - }; - return ( - <> - - - - - - - - - - - - {list?.filter(p => p.users == null).map((item, index) => ( - - - - - - ))} - -
SubjectPermissionActions
{item.clients != null ? item.clients.join(',') : item.users.join(',')} - ({ id: s, scopeName: s}))} - onRevoke={()=>false} - /> - - -
- - ) - } - -const revokeMutation = gql` - mutation RevokeSAAccess( - $prodEnvId: ID!, - $resourceId: String!, - $policyId: String!) { - deleteUmaPolicy(prodEnvId: $prodEnvId, resourceId: $resourceId, policyId: $policyId) -} -` - -export default List \ No newline at end of file diff --git a/src/nextapp/pages/index.tsx b/src/nextapp/pages/index.tsx index 25a3de035..e286cdea1 100644 --- a/src/nextapp/pages/index.tsx +++ b/src/nextapp/pages/index.tsx @@ -22,19 +22,19 @@ type HomeActions = { }; const actions: HomeActions[] = [ { - title: 'Are you a Developer?', - url: '/devportal/api-discovery', + title: 'For Developers', + url: '/devportal/api-directory', icon: FaBook, roles: [], - description: `Looking for BC Government APIs to integrate with? Go to the Directory to see what is available and to request access!`, + description: `Visit the Directory to see what APIs are available for integration.`, }, { - title: 'Are you an API Provider?', + title: 'For API Providers', url: '/manager/namespaces', icon: FaToolbox, roles: [], description: - 'Is your Ministry looking to build and share APIs? Go to Namespaces to get started!', + 'Login with BC Government credentials to start building and sharing APIs from your Ministry', }, ]; diff --git a/src/nextapp/pages/manager/consumers/[id].tsx b/src/nextapp/pages/manager/consumers/[id].tsx index bb46ef5d7..a5f501dce 100644 --- a/src/nextapp/pages/manager/consumers/[id].tsx +++ b/src/nextapp/pages/manager/consumers/[id].tsx @@ -21,8 +21,8 @@ import ControlsList from '@/components/controls-list'; import IpRestriction from '@/components/controls/ip-restriction'; import RateLimiting from '@/components/controls/rate-limiting'; import ModelIcon from '@/components/model-icon/model-icon'; +import ConsumerAuthz from '@/components/consumer-authz'; import ConsumerACL from '@/components/consumer-acl'; -import ConsumerPermissions from '@/components/consumer-permissions'; import breadcrumbs from '@/components/ns-breadcrumb'; @@ -78,15 +78,6 @@ const ConsumersPage: React.FC< ).length != 0 ).length != 0; - const hasEnvironmentWithClientCredFlow = (products: Product[]): boolean => - products - .filter((p) => p.environments.length != 0) - .filter( - (p) => - p.environments.filter((e) => e.flow == 'client-credentials').length != - 0 - ).length != 0; - return ( consumer && ( <> @@ -141,34 +132,19 @@ const ConsumersPage: React.FC< data={consumer.plugins.filter((p) => p.route || p.service)} /> - {hasEnvironmentWithAclBasedFlow(data.allProductsByNamespace) && ( - <> - - Authorization - ACL Groups - - - - - )} + - {hasEnvironmentWithClientCredFlow(data.allProductsByNamespace) && ( - <> - - Authorization - Scopes and Roles - - - - + {hasEnvironmentWithAclBasedFlow(data.allProductsByNamespace) && ( + )} @@ -205,6 +181,20 @@ const query = gql` createdAt } + allServiceAccesses(where: { consumer: { id: $id } }) { + name + consumerType + application { + appId + name + owner { + name + username + email + } + } + } + allProductsByNamespace { id name @@ -215,6 +205,7 @@ const query = gql` active flow credentialIssuer { + id availableScopes clientRoles } diff --git a/src/nextapp/pages/manager/namespace-access/index.tsx b/src/nextapp/pages/manager/namespace-access/index.tsx index a8eb6115a..8321f01a0 100644 --- a/src/nextapp/pages/manager/namespace-access/index.tsx +++ b/src/nextapp/pages/manager/namespace-access/index.tsx @@ -1,11 +1,172 @@ import * as React from 'react'; -import graphql from '@/shared/services/graphql'; import { gql } from 'graphql-request'; -const { useEffect, useState } = React; +import api, { useApi } from '@/shared/services/api'; +import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; +import { QueryClient } from 'react-query'; +import { Query } from '@/shared/types/query.types'; +import { dehydrate } from 'react-query/hydration'; +import { Box, Container, Divider, Heading, Skeleton } from '@chakra-ui/react'; +import GrantAccessDialog from '@/components/grant-access-dialog'; +import GrantServiceAccountDialog from '@/components/grant-service-account-dialog'; +import PageHeader from '@/components/page-header'; +import Head from 'next/head'; +import ResourcesManager from '@/components/resources-manager'; +import { useAuth } from '@/shared/services/auth'; +import EmptyPane from '@/components/empty-pane'; +import UsersAccessList from '@/components/users-access-list'; +import ServiceAccountsList from '@/components/service-accounts-list'; -import ResourceAccess from './resource-access'; +const Loading = ( + + + + +); -const GET_CURRENT_NS = gql` +export const getServerSideProps: GetServerSideProps = async (context) => { + const keystoneReq = context.req as any; + const nsQueryKey = ['namespaceAccess', keystoneReq.user.namespace]; + + return { + props: { + nsQueryKey, + }, + }; +}; + +const AccessRedirectPage: React.FC< + InferGetServerSidePropsType +> = ({ nsQueryKey, headers }) => { + const { user } = useAuth(); + const breadcrumbs = user + ? [ + { href: '/manager/namespaces', text: 'Namespaces' }, + { href: '/manager/namespaces', text: user.namespace }, + ] + : []; + + const { data, isSuccess, isLoading } = useApi( + nsQueryKey, + { query }, + { + suspense: false, + } + ); + + const resourceId = data?.currentNamespace?.id; + const prodEnvId = data?.currentNamespace?.prodEnvId; + + const queryKey: any = ['namespacePermissions', resourceId]; + + const { + data: permissions, + isSuccess: isPermissionsSuccess, + isLoading: isPermissionsLoading, + } = useApi( + queryKey, + { + query: permissionsQuery, + variables: { + resourceId, + prodEnvId, + }, + }, + { + enabled: isSuccess && Boolean(resourceId), + } + ); + + const requests = permissions?.getPermissionTicketsForResource.filter( + (p) => !p.granted + ); + + return ( + <> + + {`API Program Services | Resources | ${permissions?.getResourceSet.type} ${permissions?.getResourceSet.name}`} + + + 0 && ( + + ) + } + breadcrumb={breadcrumbs} + title="Namespace Access" + /> + + {isLoading || isPermissionsLoading ? ( + Loading + ) : ( + <> + + + Users with Access + + + + + p.granted + )} + resourceId={resourceId} + prodEnvId={prodEnvId} + queryKey={queryKey} + /> + + + + + Service Accounts with Access + + + + + + + )} + + + ); +}; + +export default AccessRedirectPage; + +const query = gql` query GET { currentNamespace { id @@ -18,25 +179,52 @@ const GET_CURRENT_NS = gql` } `; -const AccessRedirectPage = () => { - const [data, setData] = useState(null); - const fetch = () => { - graphql(GET_CURRENT_NS).then(({ data }) => { - setData(data.currentNamespace); - }); - }; +const permissionsQuery = gql` + query GetPermissions($resourceId: String!, $prodEnvId: ID!) { + getPermissionTicketsForResource( + prodEnvId: $prodEnvId + resourceId: $resourceId + ) { + id + owner + ownerName + requester + requesterName + resource + resourceName + scope + scopeName + granted + } - useEffect(fetch, []); + getUmaPoliciesForResource(prodEnvId: $prodEnvId, resourceId: $resourceId) { + id + name + description + type + logic + decisionStrategy + owner + clients + users + scopes + } - if (data != null) { - return ( - - ); - } - return false; -}; + getResourceSet(prodEnvId: $prodEnvId, resourceId: $resourceId) { + id + name + type + resource_scopes { + name + } + } -export default AccessRedirectPage; + Environment(where: { id: $prodEnvId }) { + name + product { + id + name + } + } + } +`; diff --git a/src/nextapp/pages/manager/namespace-access/resource-access.tsx b/src/nextapp/pages/manager/namespace-access/resource-access.tsx deleted file mode 100644 index a9cce684f..000000000 --- a/src/nextapp/pages/manager/namespace-access/resource-access.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import * as React from 'react'; -import api, { useApi } from '@/shared/services/api'; -import { Box, Container, Divider, Heading, Text } from '@chakra-ui/react'; -import EmptyPane from '@/components/empty-pane'; -import GrantAccessDialog from '@/components/grant-access-dialog'; -import GrantServiceAccountDialog from '@/components/grant-service-account-dialog'; -import Head from 'next/head'; -import PageHeader from '@/components/page-header'; -import ResourcesManager from '@/components/resources-manager'; -import UsersAccessList from '@/components/users-access-list'; -import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; -import { QueryClient } from 'react-query'; -import { getSession } from '@/shared/services/auth'; -import { Query } from '@/shared/types/query.types'; -import { dehydrate } from 'react-query/hydration'; -import { gql } from 'graphql-request'; - -import ServiceAccounts from '../../devportal/resources/service-accounts'; - -import breadcrumbs from '@/components/ns-breadcrumb'; - -interface AccessResourceProps { - queryKey: string; - variables?: { prodEnvId: string; resourceId: string }; -} - -const ApiAccessResourcePage: React.FC = ({ - queryKey, - variables, -}) => { - const { data } = useApi(queryKey, { query, variables }, { suspense: false }); - if (!data) { - return <>; - } - const { prodEnvId, resourceId } = variables; - const requests = data.getPermissionTicketsForResource?.filter( - (p) => !p.granted - ); - - const resource = data.getResourceSet; - - // title={`${resource.type} ${resource.name}`} - - return ( - <> - - {`API Program Services | Resources | Name`} - - - 0 && ( - - ) - } - breadcrumb={breadcrumbs()} - title="Namespace Access" - /> - - - Users with Access - - - - {!resourceId && ( - - )} - p.granted - )} - resourceId={resourceId} - prodEnvId={prodEnvId} - queryKey={queryKey} - /> - - - - - Service Accounts with Access - - - - - - - - ); -}; - -export default ApiAccessResourcePage; - -const query = gql` - query GetPermissions($resourceId: String!, $prodEnvId: ID!) { - getPermissionTicketsForResource( - prodEnvId: $prodEnvId - resourceId: $resourceId - ) { - id - owner - ownerName - requester - requesterName - resource - resourceName - scope - scopeName - granted - } - - getUmaPoliciesForResource(prodEnvId: $prodEnvId, resourceId: $resourceId) { - id - name - description - type - logic - decisionStrategy - owner - clients - users - scopes - } - - getResourceSet(prodEnvId: $prodEnvId, resourceId: $resourceId) { - id - name - type - resource_scopes { - name - } - } - - Environment(where: { id: $prodEnvId }) { - name - product { - id - name - } - } - } -`; diff --git a/src/nextapp/pages/manager/requests/[id].tsx b/src/nextapp/pages/manager/requests/[id].tsx index f7df19c52..2d9aefc54 100644 --- a/src/nextapp/pages/manager/requests/[id].tsx +++ b/src/nextapp/pages/manager/requests/[id].tsx @@ -43,9 +43,7 @@ import RequestActions from '@/components/request-actions'; import BusinessProfile from '@/components/business-profile'; import ActivityList from '@/components/activity-list'; import breadcrumbs from '@/components/ns-breadcrumb'; -import isString from 'lodash/isString'; - -const isNotBlank = (v: any) => isString(v) && v.length > 0; +import isNotBlank from '@/shared/isNotBlank'; export const getServerSideProps: GetServerSideProps = async (context) => { const { id } = context.params; diff --git a/src/nextapp/shared/data/links.ts b/src/nextapp/shared/data/links.ts index 8fb5d7145..7d3664ee5 100644 --- a/src/nextapp/shared/data/links.ts +++ b/src/nextapp/shared/data/links.ts @@ -14,14 +14,14 @@ const links: NavLink[] = [ // { name: 'Home', url: '/manager', access: [], sites: ['manager'] }, // { name: 'Home', url: '/devportal', access: [], sites: ['devportal'] }, { - name: 'Directory', - url: '/devportal/api-discovery', + name: 'API Directory', + url: '/devportal/api-directory', access: [], - altUrls: ['/devportal/api-discovery/[id]'], + altUrls: ['/devportal/api-directory/[id]'], sites: ['devportal'], }, { - name: 'API Access', + name: 'My Access', url: '/devportal/access', access: ['portal-user'], altUrls: [ diff --git a/src/nextapp/shared/isNotBlank.ts b/src/nextapp/shared/isNotBlank.ts new file mode 100644 index 000000000..fe80a8dd4 --- /dev/null +++ b/src/nextapp/shared/isNotBlank.ts @@ -0,0 +1,5 @@ +import isString from 'lodash/isString'; + +const isNotBlank = (v: any) => isString(v) && v.length > 0; + +export default isNotBlank; diff --git a/src/nextapp/shared/types/query.types.ts b/src/nextapp/shared/types/query.types.ts index f8b292a2d..13b110251 100644 --- a/src/nextapp/shared/types/query.types.ts +++ b/src/nextapp/shared/types/query.types.ts @@ -7142,6 +7142,7 @@ export type Mutation = { updateConsumerGroupMembership?: Maybe; linkConsumerToNamespace?: Maybe; updateConsumerRoleAssignment?: Maybe; + updateConsumerScopeAssignment?: Maybe; createNamespace?: Maybe; deleteNamespace?: Maybe; createServiceAccount?: Maybe; @@ -7885,6 +7886,14 @@ export type MutationUpdateConsumerRoleAssignmentArgs = { }; +export type MutationUpdateConsumerScopeAssignmentArgs = { + prodEnvId: Scalars['ID']; + consumerUsername: Scalars['String']; + scopeName: Scalars['String']; + grant: Scalars['Boolean']; +}; + + export type MutationCreateNamespaceArgs = { namespace: Scalars['String']; }; diff --git a/src/services/keycloak/client-registration-service.ts b/src/services/keycloak/client-registration-service.ts index d09df7d70..8734e2a0c 100644 --- a/src/services/keycloak/client-registration-service.ts +++ b/src/services/keycloak/client-registration-service.ts @@ -183,7 +183,7 @@ export class KeycloakClientRegistrationService { public async applyChanges( clientId: string, - changes: string, + changes: string[][], optional: boolean ): Promise { const addFunction = optional @@ -200,6 +200,25 @@ export class KeycloakClientRegistrationService { } } + public async syncClientScopes( + subjectClientId: string, + clientUniqueId: string, + addScopes: string[], + delScopes: string[] + ) { + const lkup = await this.kcAdminClient.clients.find({ + clientId: subjectClientId, + }); + assert.strictEqual( + lkup.length, + 1, + 'Client ID not found ' + subjectClientId + ); + const clientPK = lkup[0].id; + + await this.applyChanges(clientPK, [addScopes, delScopes], false); + } + public async syncAndApply( clientId: string, desiredSetOfDefaultScopes: string[], diff --git a/src/services/keycloak/client-service.ts b/src/services/keycloak/client-service.ts index c21aac4fa..9e304c075 100644 --- a/src/services/keycloak/client-service.ts +++ b/src/services/keycloak/client-service.ts @@ -41,7 +41,7 @@ export class KeycloakClientService { public async lookupServiceAccountUserId(id: string) { const us = await this.kcAdminClient.clients.getServiceAccountUser({ id }); - logger.debug('[lookupServiceAccountUserId] (%d) RESULT %j', id, us); + logger.debug('[lookupServiceAccountUserId] (%s) RESULT %j', id, us); return us.id; } @@ -53,6 +53,15 @@ export class KeycloakClientService { return roles; } + public async listDefaultScopes(id: string) { + logger.debug('[listDefaultScopes] For %s', id); + const scopes = await this.kcAdminClient.clients.listDefaultClientScopes({ + id, + }); + logger.debug('[listDefaultScopes] RESULT %j', scopes); + return scopes; + } + public async login(clientId: string, clientSecret: string): Promise { await this.kcAdminClient .auth({ diff --git a/src/services/keystone/gateway-service.ts b/src/services/keystone/gateway-service.ts index 1798c0d9c..0b14fcbdc 100644 --- a/src/services/keystone/gateway-service.ts +++ b/src/services/keystone/gateway-service.ts @@ -1,11 +1,17 @@ -import { Logger } from '../../logger' -import { GatewayService } from './types' +import { Logger } from '../../logger'; +import { GatewayService } from './types'; -const logger = Logger('keystone.gw-service') +const logger = Logger('keystone.gw-service'); -export async function lookupServices (context: any, serviceIds: string[]) : Promise { - const result = await context.executeGraphQL({ - query: `query GetServices($services: [ID]) { +export async function lookupServices( + context: any, + serviceIds: string[] +): Promise { + if (serviceIds.length == 0) { + return []; + } + const result = await context.executeGraphQL({ + query: `query GetServices($services: [ID]) { allGatewayServices(where: {id_in: $services}) { name plugins { @@ -21,10 +27,11 @@ export async function lookupServices (context: any, serviceIds: string[]) : Prom } } }`, - variables: { services: serviceIds }, - }) - logger.debug("Query result %j", result) - result.data.allGatewayServices.map((svc:GatewayService) => - svc.plugins?.map(plugin => plugin.config = JSON.parse(plugin.config))) - return result.data.allGatewayServices -} \ No newline at end of file + variables: { services: serviceIds }, + }); + logger.debug('Query result %j', result); + result.data.allGatewayServices.map((svc: GatewayService) => + svc.plugins?.map((plugin) => (plugin.config = JSON.parse(plugin.config))) + ); + return result.data.allGatewayServices; +} diff --git a/src/services/keystone/types.ts b/src/services/keystone/types.ts index f8b292a2d..13b110251 100644 --- a/src/services/keystone/types.ts +++ b/src/services/keystone/types.ts @@ -7142,6 +7142,7 @@ export type Mutation = { updateConsumerGroupMembership?: Maybe; linkConsumerToNamespace?: Maybe; updateConsumerRoleAssignment?: Maybe; + updateConsumerScopeAssignment?: Maybe; createNamespace?: Maybe; deleteNamespace?: Maybe; createServiceAccount?: Maybe; @@ -7885,6 +7886,14 @@ export type MutationUpdateConsumerRoleAssignmentArgs = { }; +export type MutationUpdateConsumerScopeAssignmentArgs = { + prodEnvId: Scalars['ID']; + consumerUsername: Scalars['String']; + scopeName: Scalars['String']; + grant: Scalars['Boolean']; +}; + + export type MutationCreateNamespaceArgs = { namespace: Scalars['String']; }; diff --git a/src/services/kong/consumer-service.ts b/src/services/kong/consumer-service.ts index e2dab8a91..559ebe0e2 100644 --- a/src/services/kong/consumer-service.ts +++ b/src/services/kong/consumer-service.ts @@ -169,18 +169,13 @@ export class KongConsumerService { pluginPK: string, plugin: KongPlugin ): Promise { - const { v4: uuidv4 } = require('uuid'); - - const body = { - key: uuidv4().replace(/-/g, ''), - }; - logger.debug('CALLING with ' + consumerPK + ' ' + pluginPK); + logger.debug('[updateConsumerPlugin] C=%s P=%s', consumerPK, pluginPK); const response = await fetch( `${this.kongUrl}/consumers/${consumerPK}/plugins/${pluginPK}`, { method: 'put', - body: JSON.stringify(body), + body: JSON.stringify(plugin), headers: { 'Content-Type': 'application/json', }, diff --git a/src/services/workflow/types.ts b/src/services/workflow/types.ts index ff4870d45..f0656bf24 100644 --- a/src/services/workflow/types.ts +++ b/src/services/workflow/types.ts @@ -51,7 +51,7 @@ export interface IssuerEnvironmentConfig { initialAccessToken?: string; } -export function getIssuerEnvironmentConfig( +export function checkIssuerEnvironmentConfig( issuer: CredentialIssuer, environment: string ) { @@ -59,10 +59,19 @@ export function getIssuerEnvironmentConfig( issuer.environmentDetails ); const env = details.filter((c) => c.environment === environment); + return env.length == 1 ? env[0] : null; +} + +export function getIssuerEnvironmentConfig( + issuer: CredentialIssuer, + environment: string +) { + const env = checkIssuerEnvironmentConfig(issuer, environment); + assert.strictEqual( - env.length, - 1, + env != null, + true, `EnvironmentMissing ${issuer.name} ${environment}` ); - return env[0]; + return env; } diff --git a/src/services/workflow/validate-active-environment.ts b/src/services/workflow/validate-active-environment.ts index 8b9291715..82cae1e83 100644 --- a/src/services/workflow/validate-active-environment.ts +++ b/src/services/workflow/validate-active-environment.ts @@ -46,6 +46,9 @@ export const ValidateActiveEnvironment = async ( const flow = existingItem == null ? resolvedData['flow'] : envServices.flow; + const envName = + existingItem == null ? resolvedData['name'] : envServices.name; + // The Credential Issuer says what plugins are expected // Loop through the Services to make sure the plugin is configured correctly @@ -94,7 +97,7 @@ export const ValidateActiveEnvironment = async ( 'Environment missing issuer details' ); - const envConfig = getIssuerEnvironmentConfig(issuer, envServices.name); + const envConfig = getIssuerEnvironmentConfig(issuer, envName); const isServiceMissingAllPlugins = (svc: any) => svc.plugins.filter( @@ -124,7 +127,7 @@ export const ValidateActiveEnvironment = async ( 'Environment missing issuer details' ); - const envConfig = getIssuerEnvironmentConfig(issuer, envServices.name); + const envConfig = getIssuerEnvironmentConfig(issuer, envName); const isServiceMissingAllPlugins = (svc: any) => svc.plugins.filter( diff --git a/src/test/mock-server/server.js b/src/test/mock-server/server.js index 9ce0a806f..44333866a 100644 --- a/src/test/mock-server/server.js +++ b/src/test/mock-server/server.js @@ -107,6 +107,8 @@ const server = mockServer(schemaWithMocks, { return result; }, getPermissionTickets: () => new MockList(6, (_, { id }) => ({ id })), + getPermissionTicketsForResource: () => + new MockList(6, (_, { id }) => ({ id })), getResourceSet: () => new MockList(8, (_, { id }) => ({ id })), myServiceAccesses: () => new MockList(8, (_, { id }) => ({ id })), mySelf: () => db.get('user'), diff --git a/src/test/services/workflow/validate-active-environment.test.js b/src/test/services/workflow/validate-active-environment.test.js index e99e48437..f6a22d2e7 100644 --- a/src/test/services/workflow/validate-active-environment.test.js +++ b/src/test/services/workflow/validate-active-environment.test.js @@ -1,12 +1,11 @@ import setup from './setup'; -import workflow from '../../../services/workflow'; +import { ValidateActiveEnvironment } from '../../../services/workflow'; -import { json } from './utils' +import { json } from './utils'; describe('Validate Active Environment', function () { - - const ctx = setup() + const ctx = setup(); // Enable API mocking before tests. beforeAll(() => ctx.server.listen()); @@ -14,68 +13,84 @@ describe('Validate Active Environment', function () { // Reset any runtime request handlers we may add during the tests. afterEach(() => ctx.server.resetHandlers()); - beforeEach(() => { - ctx.context.OUTPUTS = []; + beforeEach(() => { + ctx.context.OUTPUTS = []; ctx.context.Activity = []; ctx.context.IN = { - GetProductEnvironmentServices: { - data: { - allEnvironments: [ - { - name: 'ENV-NAME-1', - flow: 'kong-api-key-acl', - services: [ - { - name: 'SERVICE-1', - plugins: [ - { - name: 'acl', - config: '{}', - }, - { - name: 'key-auth', - config: '{}', - }, - ], - routes: [ - { - name: 'SERVICE-ROUTE-1', - plugins: [ - { - name: 'rate-limiting', - config: '{}', - }, - ], - }, - ], - }, - ], + GetProductEnvironmentServices: { + data: { + allEnvironments: [ + { + name: 'ENV-NAME-1', + flow: 'kong-api-key-acl', + credentialIssuer: { + id: 'ci-123', + flow: 'client-credentials', + environmentDetails: JSON.stringify([ + { environment: 'ENV-NAME-1', issuerUrl: 'http://provider' }, + ]), }, - ], + services: [ + { + name: 'SERVICE-1', + plugins: [ + { + name: 'acl', + config: '{}', + }, + { + name: 'key-auth', + config: '{}', + }, + ], + routes: [ + { + name: 'SERVICE-ROUTE-1', + plugins: [ + { + name: 'rate-limiting', + config: '{}', + }, + ], + }, + ], + }, + ], + }, + ], + }, + }, + GetCredentialIssuerById: { + data: { + CredentialIssuer: { + id: 'ci-123', + flow: 'client-credentials', + name: 'Auth Profile 123', + mode: 'auto', + environmentDetails: JSON.stringify([ + { environment: 'ENV-NAME-1', issuerUrl: 'http://provider' }, + ]), }, }, - - }; - - - }) + }, + }; + }); // Disable API mocking after the tests are done. afterAll(() => ctx.server.close()); describe('validate active environment for api key flow', function () { - it('it should succeed', async function () { const params = { existingItem: { - id: 'ENV-1', - active: true + id: 'ENV-1', + active: true, }, originalInput: {}, resolvedInput: {}, }; - await workflow.ValidateActiveEnvironment( + await ValidateActiveEnvironment( ctx.context, 'update', params.existingItem, @@ -85,67 +100,72 @@ describe('Validate Active Environment', function () { ); const expected = { - OUTPUTS: [] + OUTPUTS: [], }; expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); }); it('it should fail validation with missing acl plugin', async function () { - const params = { - existingItem: { - id: 'ENV-1', - active: true + const params = { + existingItem: { + id: 'ENV-1', + active: true, }, - originalInput: {}, - resolvedInput: {}, - }; - - ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0].services[0].plugins = [] - - await workflow.ValidateActiveEnvironment( - ctx.context, - 'update', - params.existingItem, - params.originalInput, - params.resolvedInput, - ctx.addValidationError - ); - - const expected = { - OUTPUTS: [ - { - source: 'validation', - content: - '[SERVICE-1] missing or incomplete acl or key-auth plugin.', - }, - ], - }; - expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); - }); + originalInput: {}, + resolvedInput: {}, + }; + ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0].services[0].plugins = []; + await ValidateActiveEnvironment( + ctx.context, + 'update', + params.existingItem, + params.originalInput, + params.resolvedInput, + ctx.addValidationError + ); + + const expected = { + OUTPUTS: [ + { + source: 'validation', + content: + '[SERVICE-1] missing or incomplete acl or key-auth plugin.', + }, + ], + }; + expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); + }); }); describe('validate active environment for client-credential flow', function () { - it('it should succeed', async function () { const params = { existingItem: { - id: 'ENV-1', - active: true + id: 'ENV-1', + active: true, }, originalInput: {}, resolvedInput: {}, }; - const prodEnv = ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0] - prodEnv.flow = 'client-credentials' - prodEnv.credentialIssuer = { - oidcDiscoveryUrl: "http://provider/realm/.well-known/openid-configuration" - } - prodEnv.services[0].plugins = [{ name: "jwt-keycloak", config: JSON.stringify({ well_known_template: "http://provider/realm/.well-known/openid-configuration"}) }] + const prodEnv = + ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0]; + prodEnv.flow = 'client-credentials'; + prodEnv.credentialIssuer.flow = 'client-credentials'; + + prodEnv.services[0].plugins = [ + { + name: 'jwt-keycloak', + config: JSON.stringify({ + well_known_template: + 'http://provider/realm/.well-known/openid-configuration', + }), + }, + ]; - await workflow.ValidateActiveEnvironment( + await ValidateActiveEnvironment( ctx.context, 'update', params.existingItem, @@ -155,70 +175,104 @@ describe('Validate Active Environment', function () { ); const expected = { - OUTPUTS: [] + OUTPUTS: [], }; expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); }); it('it should fail validation with missing acl plugin', async function () { - const params = { - existingItem: { - id: 'ENV-1', - active: true - }, - originalInput: {}, - resolvedInput: {}, - }; - - const prodEnv = ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0] - prodEnv.flow = 'client-credentials' - prodEnv.services[0].plugins = [] - - await workflow.ValidateActiveEnvironment( - ctx.context, - 'update', - params.existingItem, - params.originalInput, - params.resolvedInput, - ctx.addValidationError - ); - - const expected = { - OUTPUTS: [ - { - source: 'validation', - content: - '[SERVICE-1] missing or incomplete jwt-keycloak plugin.', - }, - ], - }; - expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); - }); + const params = { + existingItem: { + id: 'ENV-1', + active: true, + }, + originalInput: {}, + resolvedInput: {}, + }; + + const prodEnv = + ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0]; + prodEnv.flow = 'client-credentials'; + prodEnv.services[0].plugins = []; + await ValidateActiveEnvironment( + ctx.context, + 'update', + params.existingItem, + params.originalInput, + params.resolvedInput, + ctx.addValidationError + ); + const expected = { + OUTPUTS: [ + { + source: 'validation', + content: '[SERVICE-1] missing or incomplete jwt-keycloak plugin.', + }, + ], + }; + expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); + }); }); + describe('validate create active environment for client-credential flow', function () { + it('it should succeed', async function () { + const params = { + existingItem: null, + originalInput: { + active: true, + }, + resolvedInput: { + credentialIssuer: 1, + name: 'ENV-NAME-1', + flow: 'client-credentials', + services: [], + }, + }; - describe('validate active environment for authorization-code flow', function () { + await ValidateActiveEnvironment( + ctx.context, + 'create', + params.existingItem, + params.originalInput, + params.resolvedInput, + ctx.addValidationError + ); + const expected = { + OUTPUTS: [], + }; + expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); + }); + }); + + describe('validate active environment for authorization-code flow', function () { it('it should succeed', async function () { const params = { existingItem: { - id: 'ENV-1', - active: true + id: 'ENV-1', + active: true, }, originalInput: {}, resolvedInput: {}, }; - const prodEnv = ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0] - prodEnv.flow = 'authorization-code' - prodEnv.credentialIssuer = { - oidcDiscoveryUrl: "http://provider/realm/.well-known/openid-configuration" - } - prodEnv.services[0].plugins = [{ name: "oidc", config: JSON.stringify({ discovery: "http://provider/realm/.well-known/openid-configuration"}) }] + const prodEnv = + ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0]; + prodEnv.flow = 'authorization-code'; + prodEnv.credentialIssuer.flow = 'authorization-code'; + + prodEnv.services[0].plugins = [ + { + name: 'oidc', + config: JSON.stringify({ + discovery: 'http://provider/realm/.well-known/openid-configuration', + }), + }, + ]; - await workflow.ValidateActiveEnvironment( + await ValidateActiveEnvironment( ctx.context, 'update', params.existingItem, @@ -228,49 +282,46 @@ describe('Validate Active Environment', function () { ); const expected = { - OUTPUTS: [] + OUTPUTS: [], }; expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); }); it('it should fail validation with missing oidc plugin', async function () { - const params = { - existingItem: { - id: 'ENV-1', - active: true - }, - originalInput: {}, - resolvedInput: {}, - }; - - const prodEnv = ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0] - prodEnv.flow = 'authorization-code' - prodEnv.credentialIssuer = { - oidcDiscoveryUrl: "http://provider/realm/.well-known/openid-configuration" - } - prodEnv.services[0].plugins = [] - - await workflow.ValidateActiveEnvironment( - ctx.context, - 'update', - params.existingItem, - params.originalInput, - params.resolvedInput, - ctx.addValidationError - ); - - const expected = { - OUTPUTS: [ - { - source: 'validation', - content: - '[SERVICE-1] missing or incomplete oidc plugin.', - }, - ], - }; - expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); - }); + const params = { + existingItem: { + id: 'ENV-1', + active: true, + }, + originalInput: {}, + resolvedInput: {}, + }; + const prodEnv = + ctx.context.IN.GetProductEnvironmentServices.data.allEnvironments[0]; + prodEnv.flow = 'authorization-code'; + prodEnv.credentialIssuer.flow = 'authorization-code'; - }); + prodEnv.services[0].plugins = []; + + await ValidateActiveEnvironment( + ctx.context, + 'update', + params.existingItem, + params.originalInput, + params.resolvedInput, + ctx.addValidationError + ); + + const expected = { + OUTPUTS: [ + { + source: 'validation', + content: '[SERVICE-1] missing or incomplete oidc plugin.', + }, + ], + }; + expect(json(ctx.context.OUTPUTS)).toBe(json(expected.OUTPUTS)); + }); + }); });