Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/backend/graphene/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,10 @@ class Meta:
class ProviderCredentialsType(DjangoObjectType):
sync_count = graphene.Int()
provider = graphene.Field(ProviderType)
# Nullable: resolve_credentials withholds the value for users without
# IntegrationCredentials read β€” non-null would turn that into a hard
# error that nulls the surrounding payload.
credentials = graphene.JSONString()

class Meta:
model = ProviderCredentials
Expand Down
16 changes: 16 additions & 0 deletions backend/tests/test_graphene_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Schema-shape guards for fields with withholding resolvers."""

from graphql import GraphQLNonNull

from backend.schema import schema


def test_provider_credentials_field_is_nullable():
"""resolve_credentials returns None for users without
IntegrationCredentials read β€” the field must stay nullable, or the
withholding becomes a hard GraphQL error that nulls the surrounding
payload."""
field = schema.graphql_schema.type_map["ProviderCredentialsType"].fields[
"credentials"
]
assert not isinstance(field.type, GraphQLNonNull)
18 changes: 12 additions & 6 deletions frontend/apollo/gql.ts

Large diffs are not rendered by default.

20 changes: 14 additions & 6 deletions frontend/apollo/graphql.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion frontend/apollo/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ type ProviderCredentialsType {
id: String!
name: String!
provider: ProviderType
credentials: JSONString!
credentials: JSONString
createdAt: DateTime
updatedAt: DateTime!
syncCount: Int
Expand Down
7 changes: 5 additions & 2 deletions frontend/app/[team]/integrations/credentials/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { organisationContext } from '@/contexts/organisationContext'
import { useContext, useState, useEffect, useMemo } from 'react'
import GetOrganisationSyncs from '@/graphql/queries/syncing/GetOrgSyncs.gql'
import GetSavedCredentials from '@/graphql/queries/syncing/getSavedCredentials.gql'
import GetProviderList from '@/graphql/queries/syncing/getProviders.gql'
import { useQuery } from '@apollo/client'
import { ProviderCredentialsType, ProviderType } from '@/apollo/graphql'
Expand Down Expand Up @@ -55,7 +55,10 @@ export default function Integrations({ params }: { params: { team: string } }) {
}
}, [providerFromUrl, providers])

const { data, loading } = useQuery(GetOrganisationSyncs, {
// Deliberately not the shared GetOrganisationSyncs query: this page is the
// only consumer of savedCredentials, and resolving it decrypts every
// stored credential β€” that must not ride along on the syncs poll.
const { data, loading } = useQuery(GetSavedCredentials, {
variables: { orgId: organisation?.id },
pollInterval: 10000,
skip:
Expand Down
6 changes: 4 additions & 2 deletions frontend/components/syncing/IntegrationsHomeCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { useQuery } from '@apollo/client'
import GetSavedCredentials from '@/graphql/queries/syncing/getSavedCredentials.gql'
import GetSavedCredentialIds from '@/graphql/queries/syncing/getSavedCredentialIds.gql'
import { OrganisationType, ProviderCredentialsType } from '@/apollo/graphql'
import Spinner from '../common/Spinner'
import { FaProjectDiagram } from 'react-icons/fa'
Expand All @@ -17,7 +17,9 @@ export default function IntegrationsHomeCard(props: { organisation: Organisation
'read'
)

const { data, loading } = useQuery(GetSavedCredentials, {
// Only the count is rendered β€” an id-only selection avoids decrypting
// every stored credential on each dashboard visit.
const { data, loading } = useQuery(GetSavedCredentialIds, {
variables: {
orgId: organisation.id,
},
Expand Down
5 changes: 3 additions & 2 deletions frontend/components/syncing/UpdateProviderCredentials.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ export const UpdateProviderCredentials = (props: { credential: ProviderCredentia
const [updateCredentials] = useMutation(UpdateProviderCreds)
const [validateRotationCreds] = useMutation(ValidateRotationCredentials)
const [name, setName] = useState<string>(credential.name)
// credentials is withheld (null) without IntegrationCredentials read
const [credentials, setCredentials] = useState<CredentialState>(
JSON.parse(credential.credentials)
JSON.parse(credential.credentials || '{}') ?? {}
)

const [credentialsUpdated, setCredentialsUpdated] = useState(false)
Expand All @@ -42,7 +43,7 @@ export const UpdateProviderCredentials = (props: { credential: ProviderCredentia
const ROTATION_PROVIDER_IDS = ['litellm', 'openai']

useEffect(() => {
const credsAreEqual = isEqual(credentials, JSON.parse(credential.credentials))
const credsAreEqual = isEqual(credentials, JSON.parse(credential.credentials || '{}') ?? {})
const nameIsEqual = isEqual(name, credential.name)

setCredentialsUpdated(!credsAreEqual || !nameIsEqual)
Expand Down
14 changes: 0 additions & 14 deletions frontend/graphql/queries/syncing/GetOrgSyncs.gql
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ query GetOrganisationSyncs($orgId: ID!) {
authentication {
id
name
credentials
}
createdAt
history {
Expand All @@ -36,19 +35,6 @@ query GetOrganisationSyncs($orgId: ID!) {
meta
}
}
savedCredentials(orgId: $orgId) {
id
name
credentials
createdAt
provider {
id
name
expectedCredentials
optionalCredentials
}
syncCount
}
apps(organisationId: $orgId, appId: null) {
id
name
Expand Down
1 change: 0 additions & 1 deletion frontend/graphql/queries/syncing/getAppSyncStatus.gql
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ query GetAppSyncStatus($appId: ID!) {
authentication {
id
name
credentials
}
createdAt
history {
Expand Down
5 changes: 5 additions & 0 deletions frontend/graphql/queries/syncing/getSavedCredentialIds.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
query GetSavedCredentialIds($orgId: ID!) {
savedCredentials(orgId: $orgId) {
id
}
}
Loading