Skip to content

Commit

Permalink
fix(gate,sdk): fail fast on bad credentials before artifact upload (#961
Browse files Browse the repository at this point in the history
)

Solves
[MET-793](https://linear.app/metatypedev/issue/MET-793/artifact-upload-allowed-with-wrong-credentials)

#### Migration notes

None

- [x] The change comes with new or modified tests
- [ ] Hard-to-understand functions have explanatory comments
- [ ] End-user documentation is updated to reflect the change


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

- **New Features**
- Added a ping functionality to verify typegate connectivity and
credentials
- Introduced a new server health check mechanism across multiple
language implementations

- **Improvements**
  - Simplified error handling in deployment and query-related functions
  - Enhanced pre-deployment validation process

- **Testing**
  - Added test coverage for credential validation during deployment
  - Implemented new test scenarios for typegate connectivity checks

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
michael-0acf4 authored Jan 13, 2025
1 parent 324dffa commit eeb69c4
Show file tree
Hide file tree
Showing 11 changed files with 150 additions and 28 deletions.
3 changes: 2 additions & 1 deletion src/typegate/src/runtimes/typegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ export class TypeGateRuntime extends Runtime {
return this.execRawPrismaQuery;
case "queryPrismaModel":
return this.queryPrismaModel;

case "ping":
return (_) => true;
default:
if (name != null) {
throw new Error(`materializer '${name}' not implemented`);
Expand Down
28 changes: 26 additions & 2 deletions src/typegate/src/typegraphs/typegate.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"execRawPrismaCreate": 64,
"execRawPrismaUpdate": 65,
"execRawPrismaDelete": 66,
"queryPrismaModel": 67
"queryPrismaModel": 67,
"ping": 71
},
"id": [],
"required": [
Expand All @@ -30,7 +31,8 @@
"execRawPrismaCreate",
"execRawPrismaUpdate",
"execRawPrismaDelete",
"queryPrismaModel"
"queryPrismaModel",
"ping"
],
"policies": {
"typegraphs": [
Expand Down Expand Up @@ -68,6 +70,9 @@
],
"queryPrismaModel": [
0
],
"ping": [
0
]
}
},
Expand Down Expand Up @@ -794,6 +799,16 @@
"rowCount": [],
"data": []
}
},
{
"type": "function",
"title": "root_ping_fn",
"input": 2,
"output": 25,
"runtimeConfig": null,
"materializer": 14,
"rate_weight": null,
"rate_calls": false
}
],
"materializers": [
Expand Down Expand Up @@ -931,6 +946,15 @@
"idempotent": true
},
"data": {}
},
{
"name": "ping",
"runtime": 1,
"effect": {
"effect": "read",
"idempotent": true
},
"data": {}
}
],
"runtimes": [
Expand Down
10 changes: 10 additions & 0 deletions src/typegate/src/typegraphs/typegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ def typegate(g: Graph):
raise Exception(arg_info_by_path_id.value)
arg_info_by_path_mat = Materializer(arg_info_by_path_id.value, effect=fx.read())

ping_mat_id = runtimes.register_typegate_materializer(store, TypegateOperation.PING)
if isinstance(ping_mat_id, Err):
raise Exception(ping_mat_id.value)
ping_mat = Materializer(ping_mat_id.value, effect=fx.read())

serialized = t.gen(t.string(), serialized_typegraph_mat)

typegraph = t.struct(
Expand Down Expand Up @@ -419,4 +424,9 @@ def typegate(g: Graph):
raw_prisma_delete_mat,
),
queryPrismaModel=query_prisma_model,
ping=t.func(
t.struct({}),
t.boolean(), # always True
ping_mat,
),
)
1 change: 1 addition & 0 deletions src/typegraph/core/src/runtimes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,7 @@ impl crate::wit::runtimes::Guest for crate::Lib {
WitOp::RawPrismaUpdate => (WitEffect::Update(false), Op::RawPrismaQuery),
WitOp::RawPrismaDelete => (WitEffect::Delete(true), Op::RawPrismaQuery),
WitOp::QueryPrismaModel => (WitEffect::Read, Op::QueryPrismaModel),
WitOp::Ping => (WitEffect::Read, Op::Ping),
};

Ok(Store::register_materializer(Materializer::typegate(
Expand Down
2 changes: 2 additions & 0 deletions src/typegraph/core/src/runtimes/typegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub enum TypegateOperation {
FindPrismaModels,
RawPrismaQuery,
QueryPrismaModel,
Ping,
}

impl MaterializerConverter for TypegateOperation {
Expand All @@ -44,6 +45,7 @@ impl MaterializerConverter for TypegateOperation {
Self::FindPrismaModels => "findPrismaModels",
Self::RawPrismaQuery => "execRawPrismaQuery",
Self::QueryPrismaModel => "queryPrismaModel",
Self::Ping => "ping",
}
.to_string(),
runtime,
Expand Down
29 changes: 20 additions & 9 deletions src/typegraph/core/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ impl crate::wit::utils::Guest for crate::Lib {
.build(named_provider(&service_name)?)
}

fn gql_deploy_query(params: QueryDeployParams) -> Result<String> {
let query = r"
fn gql_deploy_query(params: QueryDeployParams) -> String {
let query = "
mutation InsertTypegraph($tg: String!, $secrets: String!, $targetVersion: String!) {
addTypegraph(fromString: $tg, secrets: $secrets, targetVersion: $targetVersion) {
name
Expand All @@ -128,8 +128,8 @@ impl crate::wit::utils::Guest for crate::Lib {

let mut secrets_map = IndexMap::new();
if let Some(secrets) = params.secrets {
for item in secrets {
secrets_map.insert(item.0, item.1);
for (secret, value) in secrets {
secrets_map.insert(secret, value);
}
}

Expand All @@ -143,11 +143,11 @@ impl crate::wit::utils::Guest for crate::Lib {
}),
});

Ok(req_body.to_string())
req_body.to_string()
}

fn gql_remove_query(names: Vec<String>) -> Result<String> {
let query = r"
fn gql_remove_query(names: Vec<String>) -> String {
let query = "
mutation($names: [String!]!) {
removeTypegraphs(names: $names)
}
Expand All @@ -156,9 +156,20 @@ impl crate::wit::utils::Guest for crate::Lib {
"query": query,
"variables": json!({
"names": names,
}),
}),
});

req_body.to_string()
}

fn gql_ping_query() -> String {
let query = "query { ping }";
let req_body = json!({
"query": query,
"variables": json!({}),
});
Ok(req_body.to_string())

req_body.to_string()
}

fn metagen_exec(config: FdkConfig) -> Result<Vec<FdkOutput>> {
Expand Down
6 changes: 4 additions & 2 deletions src/typegraph/core/wit/typegraph.wit
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@ interface runtimes {
raw-prisma-update,
raw-prisma-delete,
query-prisma-model,
ping,
}

register-typegate-materializer: func(operation: typegate-operation) -> result<materializer-id, error>;
Expand Down Expand Up @@ -638,8 +639,9 @@ interface utils {
secrets: option<list<tuple<string, string>>>,
}

gql-deploy-query: func(params: query-deploy-params) -> result<string, error>;
gql-remove-query: func(tg-name: list<string>) -> result<string, error>;
gql-deploy-query: func(params: query-deploy-params) -> string;
gql-remove-query: func(tg-name: list<string>) -> string;
gql-ping-query: func() -> string;

record fdk-config {
workspace-path: string,
Expand Down
21 changes: 20 additions & 1 deletion src/typegraph/deno/src/tg_deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ export async function tgDeploy(
if (typegate.auth) {
headers.append("Authorization", typegate.auth.asHeaderValue());
}
const url = new URL("/typegate", typegate.url);

// Make sure we have the correct credentials before doing anything
await pingTypegate(url, headers);

if (refArtifacts.length > 0) {
// upload the artifacts
Expand All @@ -103,7 +107,7 @@ export async function tgDeploy(

// deploy the typegraph
const response = (await execRequest(
new URL("/typegate", typegate.url),
url,
{
method: "POST",
headers,
Expand Down Expand Up @@ -165,3 +169,18 @@ export async function tgRemove(

return { typegate: response };
}

export async function pingTypegate(
url: URL,
headers: Headers,
) {
await execRequest(
url,
{
method: "POST",
headers,
body: wit_utils.gqlPingQuery(),
},
"Failed to access typegate",
);
}
36 changes: 24 additions & 12 deletions src/typegraph/python/typegraph/graph/tg_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@
from platform import python_version

from typegraph.gen.exports.utils import QueryDeployParams
from typegraph.gen.types import Err
from typegraph.gen.exports.core import MigrationAction, PrismaMigrationConfig
from typegraph.graph.shared_types import BasicAuth
from typegraph.graph.tg_artifact_upload import ArtifactUploader
from typegraph.graph.typegraph import TypegraphOutput
from typegraph.wit import ErrorStack, SerializeParams, store, wit_utils
from typegraph.wit import SerializeParams, store, wit_utils
from typegraph import version as sdk_version
from typegraph.io import Log

Expand Down Expand Up @@ -71,6 +70,9 @@ def tg_deploy(tg: TypegraphOutput, params: TypegraphDeployParams) -> DeployResul
if typegate.auth is not None:
headers["Authorization"] = typegate.auth.as_header_value()

# Make sure we have the correct credentials before doing anything
ping_typegate(url, headers)

serialize_params = SerializeParams(
typegraph_path=params.typegraph_path,
prefix=params.prefix,
Expand Down Expand Up @@ -104,22 +106,19 @@ def tg_deploy(tg: TypegraphOutput, params: TypegraphDeployParams) -> DeployResul
artifact_uploader.upload_artifacts()

# deploy the typegraph
res = wit_utils.gql_deploy_query(
query = wit_utils.gql_deploy_query(
store,
params=QueryDeployParams(
tg=tg_json,
secrets=[(k, v) for k, v in (params.secrets or {}).items()],
),
)

if isinstance(res, Err):
raise ErrorStack(res.value)

req = request.Request(
url=url,
method="POST",
headers=headers,
data=res.value.encode(),
data=query.encode(),
)

response = exec_request(req)
Expand Down Expand Up @@ -152,16 +151,13 @@ def tg_remove(typegraph_name: str, params: TypegraphRemoveParams):
if typegate.auth is not None:
headers["Authorization"] = typegate.auth.as_header_value()

res = wit_utils.gql_remove_query(store, [typegraph_name])

if isinstance(res, Err):
raise ErrorStack(res.value)
query = wit_utils.gql_remove_query(store, [typegraph_name])

req = request.Request(
url=url,
method="POST",
headers=headers,
data=res.value.encode(),
data=query.encode(),
)

response = exec_request(req).read().decode()
Expand All @@ -186,3 +182,19 @@ def handle_response(res: Any, url=""):
return json.loads(res)
except Exception as _:
raise Exception(f'Expected json object: got "{res}": {url}')


def ping_typegate(url: str, headers: dict[str, str]):
req = request.Request(
url=url,
method="POST",
headers=headers,
data=wit_utils.gql_ping_query(store).encode(),
)

try:
_ = request.urlopen(req)
except request.HTTPError as e:
raise Exception(f"Failed to access to typegate: {e}")
except Exception as e:
raise Exception(f"{e}: {req.full_url}")
40 changes: 40 additions & 0 deletions tests/e2e/self_deploy/self_deploy_bad_cred_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0.
// SPDX-License-Identifier: MPL-2.0
import { BasicAuth, tgDeploy } from "@typegraph/sdk/tg_deploy";

import { Meta } from "test-utils/mod.ts";
import { tg } from "./self_deploy.ts";
import { testDir } from "test-utils/dir.ts";
import { join } from "@std/path/join";
import { unreachable } from "@std/assert";
import * as path from "@std/path";
import { assertStringIncludes } from "@std/assert/string-includes";

Meta.test(
{
name: "typegate should fail after ping on bad credential",
},
async (t) => {
const gate = `http://localhost:${t.port}`;
const auth = new BasicAuth("admin", "wrong password");
const cwdDir = join(testDir, "e2e", "self_deploy");

try {
const _ = await tgDeploy(tg, {
typegate: { url: gate, auth },
secrets: {},
typegraphPath: path.join(cwdDir, "self_deploy.ts"),
migrationsDir: `${cwdDir}/prisma-migrations`,
defaultMigrationAction: {
apply: true,
create: true,
reset: false,
},
});

unreachable();
} catch(err) {
assertStringIncludes(JSON.stringify(err instanceof Error ? err.message : err), "Failed to access typegate: request failed with status 401 (Unauthorized)");
}
},
);
2 changes: 1 addition & 1 deletion tests/e2e/self_deploy/self_deploy_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Meta.test(
const { serialized, response: gateResponseAdd } = await tgDeploy(tg, {
typegate: { url: gate, auth },
secrets: {},
typegraphPath: path.join(cwdDir, "self_deploy.mjs"),
typegraphPath: path.join(cwdDir, "self_deploy.ts"),
migrationsDir: `${cwdDir}/prisma-migrations`,
defaultMigrationAction: {
apply: true,
Expand Down

0 comments on commit eeb69c4

Please sign in to comment.