@authzed/authzed-js-node
Version:
authzed js client for nodejs
658 lines (653 loc) • 28.9 kB
JavaScript
import * as grpc from "@grpc/grpc-js";
import { generateTestToken } from "./__utils__/helpers.js";
import { Struct } from "./authzedapi/google/protobuf/struct.js";
import { PreconnectServices, deadlineInterceptor } from "./util.js";
import { BulkExportRelationshipsRequest, BulkImportRelationshipsRequest, CheckPermissionRequest, CheckPermissionResponse_Permissionship, ClientSecurity, Consistency, ContextualizedCaveat, LookupResourcesRequest, LookupSubjectsRequest, NewClient, ObjectReference, PermissionsServiceClient, Relationship, RelationshipUpdate, RelationshipUpdate_Operation, SubjectReference, WriteRelationshipsRequest, WriteSchemaRequest, createStructFromObject, PbNullValue, } from "./v1.js";
import { describe, it, expect, beforeEach, vi } from "vitest";
describe("a check with an unknown namespace", () => {
it("should raise a failed precondition", () => new Promise((done) => {
const resource = ObjectReference.create({
objectType: "test/somenamespace",
objectId: "bar",
});
const testUser = ObjectReference.create({
objectType: "test/user",
objectId: "someuser",
});
const checkPermissionRequest = CheckPermissionRequest.create({
resource,
permission: "someperm",
subject: SubjectReference.create({
object: testUser,
}),
});
const client = NewClient(generateTestToken("v1-test-unknown"), "localhost:50051", ClientSecurity.INSECURE_LOCALHOST_ALLOWED);
client.checkPermission(checkPermissionRequest, function (err, response) {
expect(response).toBe(undefined);
expect(err?.code).toBe(grpc.status.FAILED_PRECONDITION);
client.close();
done();
});
}));
});
describe("a check with an known namespace", () => {
it("should succeed", () => new Promise((done) => {
// Write some schema.
const client = NewClient(generateTestToken("v1-namespace"), "localhost:50051", ClientSecurity.INSECURE_LOCALHOST_ALLOWED, PreconnectServices.PERMISSIONS_SERVICE |
PreconnectServices.SCHEMA_SERVICE);
const request = WriteSchemaRequest.create({
schema: `definition test/user {}
definition test/document {
relation viewer: test/user
permission view = viewer
}
`,
});
new Promise((resolve) => {
client.writeSchema(request, function (err, response) {
expect(err).toBe(null);
resolve(response);
});
})
.then((schemaResponse) => {
expect(schemaResponse).toBeTruthy();
return new Promise((resolve) => {
// Write a relationship.
const resource = ObjectReference.create({
objectType: "test/document",
objectId: "somedocument",
});
const testUser = ObjectReference.create({
objectType: "test/user",
objectId: "someuser",
});
const writeRequest = WriteRelationshipsRequest.create({
updates: [
RelationshipUpdate.create({
relationship: Relationship.create({
resource: resource,
relation: "viewer",
subject: SubjectReference.create({
object: testUser,
}),
}),
operation: RelationshipUpdate_Operation.CREATE,
}),
],
});
client.writeRelationships(writeRequest, function (err, response) {
expect(err).toBe(null);
resolve({ response, resource, testUser });
});
});
})
.then((vals) => {
const { response, resource, testUser } = vals;
expect(response).toBeTruthy();
return new Promise((resolve) => {
// Call check.
const checkPermissionRequest = CheckPermissionRequest.create({
resource,
permission: "view",
subject: SubjectReference.create({
object: testUser,
}),
consistency: Consistency.create({
requirement: {
oneofKind: "fullyConsistent",
fullyConsistent: true,
},
}),
});
client.checkPermission(checkPermissionRequest, function (err, response) {
expect(err).toBe(null);
resolve(response);
});
});
})
.then((response) => {
const checkResponse = response;
expect(checkResponse?.permissionship).toBe(CheckPermissionResponse_Permissionship.HAS_PERMISSION);
client.close();
done();
});
}));
describe("with caveated relations", () => {
it("should succeed", () => new Promise((done) => {
// Write some schema.
const client = NewClient(generateTestToken("v1-namespace-caveats"), "localhost:50051", ClientSecurity.INSECURE_LOCALHOST_ALLOWED);
const request = WriteSchemaRequest.create({
schema: `definition test/user {}
caveat has_special_attribute(special bool) {
special == true
}
definition test/document {
relation viewer: test/user
relation caveated_viewer: test/user with has_special_attribute
permission view = viewer + caveated_viewer
}
`,
});
new Promise((resolve) => {
client.writeSchema(request, function (err, response) {
expect(err).toBe(null);
resolve(response);
});
})
.then((schemaResponse) => {
expect(schemaResponse).toBeTruthy();
return new Promise((resolve) => {
// Write a relationship.
const resource = ObjectReference.create({
objectType: "test/document",
objectId: "somedocument",
});
const testUser = ObjectReference.create({
objectType: "test/user",
objectId: "specialuser",
});
const writeRequest = WriteRelationshipsRequest.create({
updates: [
RelationshipUpdate.create({
relationship: Relationship.create({
resource: resource,
relation: "caveated_viewer",
subject: SubjectReference.create({
object: testUser,
}),
optionalCaveat: ContextualizedCaveat.create({
caveatName: "has_special_attribute",
}),
}),
operation: RelationshipUpdate_Operation.CREATE,
}),
],
});
client.writeRelationships(writeRequest, function (err, response) {
expect(err).toBe(null);
resolve({ response, resource, testUser });
});
});
})
.then((vals) => {
const { response, resource, testUser } = vals;
expect(response).toBeTruthy();
return new Promise((resolve) => {
// Call check when user has special attribute.
const checkPermissionRequest = CheckPermissionRequest.create({
resource,
permission: "view",
subject: SubjectReference.create({
object: testUser,
}),
consistency: Consistency.create({
requirement: {
oneofKind: "fullyConsistent",
fullyConsistent: true,
},
}),
context: Struct.fromJson({ special: true }),
});
client.checkPermission(checkPermissionRequest, function (err, response) {
expect(err).toBe(null);
resolve(response);
});
});
})
.then((response) => {
const checkResponse = response;
expect(checkResponse?.permissionship).toBe(CheckPermissionResponse_Permissionship.HAS_PERMISSION);
client.close();
done();
});
}));
});
});
describe("Lookup APIs", () => {
let token;
beforeEach(() => new Promise((done) => {
token = generateTestToken("v1-lookup");
const client = NewClient(token, "localhost:50051", ClientSecurity.INSECURE_LOCALHOST_ALLOWED);
const request = WriteSchemaRequest.create({
schema: `definition test/user {}
definition test/document {
relation viewer: test/user
permission view = viewer
}
`,
});
const resource = ObjectReference.create({
objectType: "test/document",
objectId: "somedocument",
});
const writeRequest = WriteRelationshipsRequest.create({
updates: [
RelationshipUpdate.create({
relationship: Relationship.create({
resource: resource,
relation: "viewer",
subject: SubjectReference.create({
object: ObjectReference.create({
objectType: "test/user",
objectId: "someuser",
}),
}),
}),
operation: RelationshipUpdate_Operation.CREATE,
}),
RelationshipUpdate.create({
relationship: Relationship.create({
resource: resource,
relation: "viewer",
subject: SubjectReference.create({
object: ObjectReference.create({
objectType: "test/user",
objectId: "someuser2",
}),
}),
}),
operation: RelationshipUpdate_Operation.CREATE,
}),
],
});
client.writeSchema(request, function (err) {
expect(err).toBe(null);
client.writeRelationships(writeRequest, function (err) {
expect(err).toBe(null);
done();
});
});
}));
it("can lookup subjects", () => new Promise((done, fail) => {
const client = NewClient(token, "localhost:50051", ClientSecurity.INSECURE_LOCALHOST_ALLOWED);
const request = LookupSubjectsRequest.create({
resource: ObjectReference.create({
objectType: "test/document",
objectId: "somedocument",
}),
permission: "view",
subjectObjectType: "test/user",
consistency: Consistency.create({
requirement: {
oneofKind: "fullyConsistent",
fullyConsistent: true,
},
}),
});
const resStream = client.lookupSubjects(request);
resStream.on("data", function (subject) {
expect(["someuser", "someuser2"]).toContain(subject.subject?.subjectObjectId);
});
resStream.on("end", function () {
client.close();
done();
});
resStream.on("error", function (e) {
client.close();
fail(e);
});
resStream.on("status", function (status) {
expect(status.code).toEqual(grpc.status.OK);
});
}));
it("can lookup resources", () => new Promise((done, fail) => {
const client = NewClient(token, "localhost:50051", ClientSecurity.INSECURE_LOCALHOST_ALLOWED);
const request = LookupResourcesRequest.create({
subject: SubjectReference.create({
object: ObjectReference.create({
objectType: "test/user",
objectId: "someuser",
}),
}),
permission: "view",
resourceObjectType: "test/document",
consistency: Consistency.create({
requirement: {
oneofKind: "fullyConsistent",
fullyConsistent: true,
},
}),
});
const resStream = client.lookupResources(request);
resStream.on("data", function (response) {
expect(response.resourceObjectId).toEqual("somedocument");
});
resStream.on("end", function () {
client.close();
done();
});
resStream.on("error", function (e) {
client.close();
fail(e);
});
resStream.on("status", function (status) {
expect(status.code).toEqual(grpc.status.OK);
});
}));
});
describe("a check with a negative timeout", () => {
it("should fail immediately", () => new Promise((done) => {
const resource = ObjectReference.create({
objectType: "test/somenamespace",
objectId: "bar",
});
const testUser = ObjectReference.create({
objectType: "test/user",
objectId: "someuser",
});
const checkPermissionRequest = CheckPermissionRequest.create({
resource,
permission: "someperm",
subject: SubjectReference.create({
object: testUser,
}),
});
const client = NewClient(generateTestToken("v1-test-unknown"), "localhost:50051", ClientSecurity.INSECURE_LOCALHOST_ALLOWED, PreconnectServices.NONE, {
interceptors: [deadlineInterceptor(-100)],
});
client.checkPermission(checkPermissionRequest, function (err, response) {
expect(response).toBe(undefined);
expect(err?.code).toBe(grpc.status.DEADLINE_EXCEEDED);
client.close();
done();
});
}));
});
describe("Experimental Service", () => {
let token;
beforeEach(() => new Promise((done) => {
token = generateTestToken("v1-experimental-service");
const client = NewClient(token, "localhost:50051", ClientSecurity.INSECURE_LOCALHOST_ALLOWED);
const request = WriteSchemaRequest.create({
schema: `definition test/user {}
definition test/document {
relation viewer: test/user
permission view = viewer
}
`,
});
client.writeSchema(request, function (err) {
expect(err).toBe(null);
client.close();
done();
});
}));
it("can bulk import relationships", () => new Promise((done, fail) => {
const client = NewClient(token, "localhost:50051", ClientSecurity.INSECURE_LOCALHOST_ALLOWED);
const writeStream = client.bulkImportRelationships((err, value) => {
if (err) {
fail(err);
}
expect(value?.numLoaded).toEqual("2");
client.close();
done();
});
writeStream.on("error", (e) => {
fail(e);
});
const resource = ObjectReference.create({
objectType: "test/document",
objectId: "somedocument",
});
writeStream.write(BulkImportRelationshipsRequest.create({
relationships: [
Relationship.create({
resource: resource,
relation: "viewer",
subject: SubjectReference.create({
object: ObjectReference.create({
objectType: "test/user",
objectId: "someuser",
}),
}),
}),
Relationship.create({
resource: resource,
relation: "viewer",
subject: SubjectReference.create({
object: ObjectReference.create({
objectType: "test/user",
objectId: "someuser2",
}),
}),
}),
],
}));
writeStream.end();
}));
it("can bulk export relationships", () => new Promise((done, fail) => {
const client = NewClient(token, "localhost:50051", ClientSecurity.INSECURE_LOCALHOST_ALLOWED);
const resource = ObjectReference.create({
objectType: "test/document",
objectId: "somedocument",
});
const writeRequest = WriteRelationshipsRequest.create({
updates: [
RelationshipUpdate.create({
relationship: Relationship.create({
resource: resource,
relation: "viewer",
subject: SubjectReference.create({
object: ObjectReference.create({
objectType: "test/user",
objectId: "someuser",
}),
}),
}),
operation: RelationshipUpdate_Operation.CREATE,
}),
RelationshipUpdate.create({
relationship: Relationship.create({
resource: resource,
relation: "viewer",
subject: SubjectReference.create({
object: ObjectReference.create({
objectType: "test/user",
objectId: "someuser2",
}),
}),
}),
operation: RelationshipUpdate_Operation.CREATE,
}),
],
});
client.writeRelationships(writeRequest, function (err) {
expect(err).toBe(null);
const resStream = client.bulkExportRelationships(BulkExportRelationshipsRequest.create({
consistency: Consistency.create({
requirement: {
oneofKind: "fullyConsistent",
fullyConsistent: true,
},
}),
}));
resStream.on("data", function (response) {
expect(response.relationships).toEqual([
{
relation: "viewer",
resource: {
objectType: "test/document",
objectId: "somedocument",
},
subject: {
optionalRelation: "",
object: { objectType: "test/user", objectId: "someuser" },
},
},
{
relation: "viewer",
resource: {
objectType: "test/document",
objectId: "somedocument",
},
subject: {
optionalRelation: "",
object: { objectType: "test/user", objectId: "someuser2" },
},
},
]);
});
resStream.on("end", function () {
client.close();
done();
});
resStream.on("error", function (e) {
client.close();
fail(e);
});
resStream.on("status", function (status) {
expect(status.code).toEqual(grpc.status.OK);
});
});
}));
describe("WriteRelationships with transaction metadata (Integration Test)", () => {
it("should successfully write relationships with metadata and verify metadata transmission", () => new Promise((done, fail) => {
const testToken = generateTestToken("v1-int-tx-metadata");
const client = NewClient(testToken, "localhost:50051", ClientSecurity.INSECURE_LOCALHOST_ALLOWED, PreconnectServices.SCHEMA_SERVICE |
PreconnectServices.PERMISSIONS_SERVICE);
const writeSpy = vi.spyOn(PermissionsServiceClient.prototype, "writeRelationships");
const schema = `
definition test/user {}
definition test/document {
relation viewer: test/user
permission view = viewer
}
`;
const writeSchemaRequest = WriteSchemaRequest.create({ schema });
client.writeSchema(writeSchemaRequest, (schemaErr, schemaResponse) => {
if (schemaErr) {
client.close();
fail(schemaErr);
return;
}
expect(schemaResponse).toBeDefined();
const uniqueSuffix = Date.now();
const resource = ObjectReference.create({
objectType: "test/document",
objectId: `doc-${uniqueSuffix}`,
});
const user = ObjectReference.create({
objectType: "test/user",
objectId: `user-${uniqueSuffix}`,
});
const updates = [
RelationshipUpdate.create({
relationship: Relationship.create({
resource,
relation: "viewer",
subject: SubjectReference.create({ object: user }),
}),
operation: RelationshipUpdate_Operation.CREATE,
}),
];
const metadataObject = {
transaction_id: "test-tx-123",
other_data: "sample",
};
const transactionMetadata = createStructFromObject(metadataObject);
const writeRequest = WriteRelationshipsRequest.create({
updates,
optionalTransactionMetadata: transactionMetadata,
});
client.writeRelationships(writeRequest, (err, response) => {
if (err) {
client.close();
fail(err);
return;
}
expect(err).toBeNull();
expect(response).toBeDefined();
expect(response?.writtenAt).toBeDefined();
expect(writeSpy).toHaveBeenCalledTimes(1);
const actualRequest = writeSpy.mock
.calls[0][0];
expect(actualRequest.updates).toEqual(updates);
expect(actualRequest.optionalTransactionMetadata).toBeDefined();
expect(actualRequest.optionalTransactionMetadata).toEqual(transactionMetadata);
const transactionIdField = actualRequest.optionalTransactionMetadata?.fields?.["transaction_id"];
expect(transactionIdField?.kind?.oneofKind).toBe("stringValue");
if (transactionIdField?.kind?.oneofKind === "stringValue") {
expect(transactionIdField.kind.stringValue).toBe("test-tx-123");
}
const otherDataField = actualRequest.optionalTransactionMetadata?.fields?.["other_data"];
expect(otherDataField?.kind?.oneofKind).toBe("stringValue");
if (otherDataField?.kind?.oneofKind === "stringValue") {
expect(otherDataField.kind.stringValue).toBe("sample");
}
client.close();
done();
});
});
}));
});
});
describe("createStructFromObject unit tests", () => {
it("should convert a simple JS object with primitive types", () => {
const obj = {
stringProp: "hello",
numberProp: 123,
booleanProp: true,
};
const struct = createStructFromObject(obj);
expect(struct.fields.stringProp?.kind.oneofKind).toBe("stringValue");
expect(struct.fields.stringProp?.kind.oneofKind === "stringValue" &&
struct.fields.stringProp?.kind.stringValue).toBe("hello");
expect(struct.fields.numberProp?.kind.oneofKind).toBe("numberValue");
expect(struct.fields.numberProp?.kind.oneofKind === "numberValue" &&
struct.fields.numberProp?.kind.numberValue).toBe(123);
expect(struct.fields.booleanProp?.kind.oneofKind).toBe("boolValue");
expect(struct.fields.booleanProp?.kind.oneofKind === "boolValue" &&
struct.fields.booleanProp?.kind.boolValue).toBe(true);
});
it("should convert a JS object with null values", () => {
const obj = {
nullProp: null,
};
const struct = createStructFromObject(obj);
expect(struct.fields.nullProp?.kind.oneofKind).toBe("nullValue");
expect(struct.fields.nullProp?.kind.oneofKind === "nullValue" &&
struct.fields.nullProp?.kind.nullValue).toBe(PbNullValue.NULL_VALUE);
});
it("should convert a JS object with nested objects", () => {
const obj = {
nestedProp: {
innerString: "world",
innerNumber: 456,
},
};
const struct = createStructFromObject(obj);
const nestedStruct = struct.fields.nestedProp?.kind.oneofKind === "structValue" &&
struct.fields.nestedProp.kind.structValue;
expect(nestedStruct).toBeTruthy();
if (nestedStruct) {
expect(nestedStruct.fields.innerString?.kind.oneofKind).toBe("stringValue");
expect(nestedStruct.fields.innerString?.kind.oneofKind === "stringValue" &&
nestedStruct.fields.innerString?.kind.stringValue).toBe("world");
expect(nestedStruct.fields.innerNumber?.kind.oneofKind).toBe("numberValue");
expect(nestedStruct.fields.innerNumber?.kind.oneofKind === "numberValue" &&
nestedStruct.fields.innerNumber?.kind.numberValue).toBe(456);
}
});
it("should convert an empty JS object to an empty Struct", () => {
const obj = {};
const struct = createStructFromObject(obj);
expect(Object.keys(struct.fields).length).toBe(0);
});
it("should throw an error for null input", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => createStructFromObject(null)).toThrow("Input data for createStructFromObject must be a non-null object.");
});
it("should throw an error for non-object input (string)", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => createStructFromObject("not an object")).toThrow("Input data for createStructFromObject must be a non-null object.");
});
it("should throw an error for non-object input (number)", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => createStructFromObject(123)).toThrow("Input data for createStructFromObject must be a non-null object.");
});
it("should throw an error for array input", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => createStructFromObject([])).toThrow("Input data for createStructFromObject must be a non-null object.");
});
});
//# sourceMappingURL=v1.test.js.map