From 82e8d129b7df60e7fea7ea011eda72ffef09f0f7 Mon Sep 17 00:00:00 2001 From: Raghd Hamzeh Date: Fri, 3 Nov 2023 17:41:23 -0400 Subject: [PATCH] chore: change tokens and wip conditional support (#173) * chore: change tokens and wip conditional support * chore: run prettier * feat(consitions): add basic suggestions * fix: export GraphNodeGroup and GraphEdgeGroup --- src/constants/keyword.ts | 2 + src/theme/theme.typings.ts | 50 ++++++----- src/theme/utils.ts | 6 ++ src/tools/monaco/language-definition.ts | 22 ++++- src/tools/monaco/providers/completion.ts | 16 ++++ src/tools/monaco/theme.ts | 2 +- src/utilities/graphs/graph.typings.ts | 2 + src/utilities/graphs/index.ts | 2 +- src/utilities/graphs/model-graph.ts | 107 ++++++++++++++++++++--- 9 files changed, 170 insertions(+), 39 deletions(-) diff --git a/src/constants/keyword.ts b/src/constants/keyword.ts index 3b67ce8..715c23a 100644 --- a/src/constants/keyword.ts +++ b/src/constants/keyword.ts @@ -7,9 +7,11 @@ export enum Keyword { OR = "or", AND = "and", FROM = "from", + WITH = "with", BUT_NOT = "but not", MODEL = "model", SCHEMA = "schema", + CONDITION = "condition", } export enum ReservedKeywords { diff --git a/src/theme/theme.typings.ts b/src/theme/theme.typings.ts index 56f2195..023d152 100644 --- a/src/theme/theme.typings.ts +++ b/src/theme/theme.typings.ts @@ -9,32 +9,38 @@ export enum OpenFgaDslThemeTokenType { export enum OpenFgaDslThemeToken { COMMENT = "comment", - DELIMITER_BRACKET_RELATION_DEFINITION = "relation-definition.bracket.delimiter", - DELIMITER_BRACKET_TYPE_RESTRICTIONS = "type-restrictions.bracket.delimiter", - DELIMITER_COLON_TYPE_RESTRICTIONS = "colon.type-restrictions.delimiter", - DELIMITER_COMMA_TYPE_RESTRICTIONS = "comma.type-restrictions.delimiter", - DELIMITER_DEFINE_COLON = "colon.define.delimiter", - DELIMITER_HASHTAG_TYPE_RESTRICTIONS = "hashtag.type-restrictions.delimiter", - KEYWORD_AS = "as.keyword", - KEYWORD_DEFINE = "define.keyword", - KEYWORD_FROM = "from.keyword", - KEYWORD_MODEL = "model.keyword", - KEYWORD_RELATIONS = "relations.keyword", - KEYWORD_SCHEMA = "schema.keyword", - KEYWORD_SELF = "self.keyword", - KEYWORD_TYPE = "type.keyword", - OPERATOR_AND = "intersection.operator", - OPERATOR_BUT_NOT = "exclusion.operator", - OPERATOR_OR = "union.operator", + DELIMITER_BRACKET_RELATION_DEFINITION = "delimiter.bracket.relation-definition", + DELIMITER_BRACKET_TYPE_RESTRICTIONS = "delimiter.bracket.type-restrictions", + DELIMITER_BRACKET_CONDITION_EXPRESSION = "delimiter.bracket.condition-expression", + DELIMITER_COLON_TYPE_RESTRICTIONS = "delimiter.colon.type-restrictions", + DELIMITER_COMMA_TYPE_RESTRICTIONS = "delimiter.comma.type-restrictions", + DELIMITER_DEFINE_COLON = "delimiter.colon.define", + DELIMITER_HASHTAG_TYPE_RESTRICTIONS = "delimiter.hashtag.type-restrictions", + KEYWORD_AS = "keyword.as", + KEYWORD_DEFINE = "keyword.define", + KEYWORD_FROM = "keyword.from", + KEYWORD_MODEL = "keyword.model", + KEYWORD_RELATIONS = "keyword.relations", + KEYWORD_SCHEMA = "keyword.schema", + KEYWORD_SELF = "keyword.self", + KEYWORD_TYPE = "keyword.type", + KEYWORD_CONDITION = "keyword.condition", + KEYWORD_WITH = "keyword.with", + OPERATOR_AND = "keyword.operator.word.intersection", + OPERATOR_BUT_NOT = "keyword.operator.word.exclusion", + OPERATOR_OR = "keyword.operator.word.union", + VALUE_CONDITION = "entity.name.function.condition", VALUE_RELATION_COMPUTED = "computed.relation.value", - VALUE_RELATION_NAME = "name.relation.value", + VALUE_RELATION_NAME = "entity.name.function.member.relation.name", VALUE_RELATION_TUPLE_TO_USERSET_COMPUTED = "computed.tupletouserset.relation.value", VALUE_RELATION_TUPLE_TO_USERSET_TUPLESET = "tupleset.tupletouserset.relation.value", VALUE_SCHEMA = "schema.value", - VALUE_TYPE_NAME = "name.type.value", - VALUE_TYPE_RESTRICTIONS_RELATION = "relation.type-restrictions.value", - VALUE_TYPE_RESTRICTIONS_TYPE = "type.type-restrictions.value", - VALUE_TYPE_RESTRICTIONS_WILDCARD = "wildcard.type-restrictions.value", + VALUE_TYPE_NAME = "support.class.type.name.value", + VALUE_TYPE_RESTRICTIONS_RELATION = "variable.parameter.type-restrictions.relation.value", + VALUE_TYPE_RESTRICTIONS_TYPE = "variable.parameter.type-restrictions.type.value", + VALUE_TYPE_RESTRICTIONS_WILDCARD = "variable.parameter.type-restrictions.wildcard.value", + CONDITION_PARAM = "variable.parameter.name.condition", + CONDITION_PARAM_TYPE = "variable.parameter.type.condition", } export interface OpenFgaThemeConfiguration { diff --git a/src/theme/utils.ts b/src/theme/utils.ts index c32d7a4..ae4a289 100644 --- a/src/theme/utils.ts +++ b/src/theme/utils.ts @@ -4,6 +4,7 @@ const tokenTypeMap: Record = { [OpenFgaDslThemeToken.COMMENT]: OpenFgaDslThemeTokenType.COMMENT, [OpenFgaDslThemeToken.DELIMITER_BRACKET_RELATION_DEFINITION]: OpenFgaDslThemeTokenType.DEFAULT, [OpenFgaDslThemeToken.DELIMITER_BRACKET_TYPE_RESTRICTIONS]: OpenFgaDslThemeTokenType.DIRECTLY_ASSIGNABLE, + [OpenFgaDslThemeToken.DELIMITER_BRACKET_CONDITION_EXPRESSION]: OpenFgaDslThemeTokenType.DEFAULT, [OpenFgaDslThemeToken.DELIMITER_COLON_TYPE_RESTRICTIONS]: OpenFgaDslThemeTokenType.DIRECTLY_ASSIGNABLE, [OpenFgaDslThemeToken.DELIMITER_COMMA_TYPE_RESTRICTIONS]: OpenFgaDslThemeTokenType.DIRECTLY_ASSIGNABLE, [OpenFgaDslThemeToken.DELIMITER_DEFINE_COLON]: OpenFgaDslThemeTokenType.DEFAULT, @@ -19,6 +20,9 @@ const tokenTypeMap: Record = { [OpenFgaDslThemeToken.OPERATOR_AND]: OpenFgaDslThemeTokenType.KEYWORD, [OpenFgaDslThemeToken.OPERATOR_BUT_NOT]: OpenFgaDslThemeTokenType.KEYWORD, [OpenFgaDslThemeToken.OPERATOR_OR]: OpenFgaDslThemeTokenType.KEYWORD, + [OpenFgaDslThemeToken.KEYWORD_CONDITION]: OpenFgaDslThemeTokenType.KEYWORD, + [OpenFgaDslThemeToken.KEYWORD_WITH]: OpenFgaDslThemeTokenType.KEYWORD, + [OpenFgaDslThemeToken.VALUE_CONDITION]: OpenFgaDslThemeTokenType.TYPE, [OpenFgaDslThemeToken.VALUE_RELATION_COMPUTED]: OpenFgaDslThemeTokenType.DEFAULT, [OpenFgaDslThemeToken.VALUE_RELATION_NAME]: OpenFgaDslThemeTokenType.RELATION, [OpenFgaDslThemeToken.VALUE_RELATION_TUPLE_TO_USERSET_COMPUTED]: OpenFgaDslThemeTokenType.DEFAULT, @@ -28,6 +32,8 @@ const tokenTypeMap: Record = { [OpenFgaDslThemeToken.VALUE_TYPE_RESTRICTIONS_RELATION]: OpenFgaDslThemeTokenType.DIRECTLY_ASSIGNABLE, [OpenFgaDslThemeToken.VALUE_TYPE_RESTRICTIONS_TYPE]: OpenFgaDslThemeTokenType.DIRECTLY_ASSIGNABLE, [OpenFgaDslThemeToken.VALUE_TYPE_RESTRICTIONS_WILDCARD]: OpenFgaDslThemeTokenType.DIRECTLY_ASSIGNABLE, + [OpenFgaDslThemeToken.CONDITION_PARAM]: OpenFgaDslThemeTokenType.RELATION, + [OpenFgaDslThemeToken.CONDITION_PARAM_TYPE]: OpenFgaDslThemeTokenType.DEFAULT, }; export function getThemeTokenStyle( diff --git a/src/tools/monaco/language-definition.ts b/src/tools/monaco/language-definition.ts index 968bb57..7939d07 100644 --- a/src/tools/monaco/language-definition.ts +++ b/src/tools/monaco/language-definition.ts @@ -17,10 +17,12 @@ export function getLanguageConfiguration(monaco: typeof MonacoEditor): MonacoEdi brackets: [ ["[", "]"], ["(", ")"], + ["{", "}"], ], autoClosingPairs: [ { open: "[", close: "]" }, { open: "(", close: ")" }, + { open: "{", close: "}" }, ], surroundingPairs: [ { open: "[", close: "]" }, @@ -54,6 +56,7 @@ export const language = { brackets: [ { open: "[", close: "]", token: OpenFgaDslThemeToken.DELIMITER_BRACKET_TYPE_RESTRICTIONS }, { open: "(", close: ")", token: OpenFgaDslThemeToken.DELIMITER_BRACKET_RELATION_DEFINITION }, + { open: "{", close: "}", token: OpenFgaDslThemeToken.DELIMITER_BRACKET_CONDITION_EXPRESSION }, ], tokenizer: { @@ -81,7 +84,7 @@ export const language = { [ "@brackets", "@whitespace", - "type.type-restrictions.value", + OpenFgaDslThemeToken.VALUE_TYPE_RESTRICTIONS_TYPE, "@whitespace", OpenFgaDslThemeToken.DELIMITER_COMMA_TYPE_RESTRICTIONS, ], @@ -112,7 +115,7 @@ export const language = { new RegExp(/(but not)(\s+)(@identifiers)/), [OpenFgaDslThemeToken.OPERATOR_BUT_NOT, "@whitespace", OpenFgaDslThemeToken.VALUE_RELATION_COMPUTED], ], - + [new RegExp(/(\s+)(with)(\s+)/), ["@whitespace", OpenFgaDslThemeToken.KEYWORD_WITH, "@whitespace"]], [ new RegExp(/(as)(\s+)(@identifiers)/), [OpenFgaDslThemeToken.KEYWORD_AS, "@whitespace", OpenFgaDslThemeToken.VALUE_RELATION_COMPUTED], @@ -121,6 +124,19 @@ export const language = { new RegExp(/(:)(\s+)(@identifiers)/), [OpenFgaDslThemeToken.DELIMITER_DEFINE_COLON, "@whitespace", OpenFgaDslThemeToken.VALUE_RELATION_COMPUTED], ], + [ + new RegExp(/(@identifiers)(:)(\s+)(@identifiers)/), + [ + OpenFgaDslThemeToken.CONDITION_PARAM, + OpenFgaDslThemeToken.DELIMITER_DEFINE_COLON, + "@whitespace", + OpenFgaDslThemeToken.CONDITION_PARAM_TYPE, + ], + ], + [ + new RegExp(/(condition)(\s)(@identifiers)(\()/), + [OpenFgaDslThemeToken.KEYWORD_CONDITION, "@whitespace", OpenFgaDslThemeToken.VALUE_CONDITION, "@brackets"], + ], [ new RegExp(/(@identifiers)(\s+)(from)(\s+)(@identifiers)/), [ @@ -162,6 +178,8 @@ export const language = { [Keyword.RELATIONS]: OpenFgaDslThemeToken.KEYWORD_RELATIONS, [Keyword.DEFINE]: OpenFgaDslThemeToken.KEYWORD_DEFINE, [Keyword.FROM]: OpenFgaDslThemeToken.KEYWORD_FROM, + [Keyword.WITH]: OpenFgaDslThemeToken.KEYWORD_WITH, + [Keyword.CONDITION]: OpenFgaDslThemeToken.KEYWORD_CONDITION, [Keyword.AS]: OpenFgaDslThemeToken.KEYWORD_AS, [Keyword.MODEL]: OpenFgaDslThemeToken.KEYWORD_MODEL, [Keyword.SCHEMA]: { token: OpenFgaDslThemeToken.KEYWORD_SCHEMA }, diff --git a/src/tools/monaco/providers/completion.ts b/src/tools/monaco/providers/completion.ts index c0d36bd..dbd8cae 100644 --- a/src/tools/monaco/providers/completion.ts +++ b/src/tools/monaco/providers/completion.ts @@ -109,6 +109,16 @@ ${SINGLE_INDENTATION}${Keyword.SCHEMA} \${1:1.1}`, insertText: Keyword.TYPE, range, }, + { + label: Keyword.CONDITION, + kind: monaco.languages.CompletionItemKind.Function, + // eslint-disable-next-line no-template-curly-in-string + insertText: `${Keyword.CONDITION} \${1:conditionName}(\${2:parameterName}: \${3:string}) { + \${4} +}`, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + }, ]; } else if (position.column === 4) { suggestions = [ @@ -153,6 +163,12 @@ ${SINGLE_INDENTATION}${Keyword.SCHEMA} \${1:1.1}`, insertText: Keyword.FROM, range, }, + { + label: Keyword.CONDITION, + kind: monaco.languages.CompletionItemKind.Keyword, + insertText: Keyword.CONDITION, + range, + }, ]; } else if (position.column === 6) { suggestions = [ diff --git a/src/tools/monaco/theme.ts b/src/tools/monaco/theme.ts index 70c17c0..c97ddc9 100644 --- a/src/tools/monaco/theme.ts +++ b/src/tools/monaco/theme.ts @@ -7,7 +7,7 @@ import { getThemeTokenStyle } from "../../theme/utils"; function buildMonacoTheme(themeConfig: OpenFgaThemeConfiguration): editor.IStandaloneThemeData { return { base: themeConfig.baseTheme || "vs", - inherit: false, + inherit: true, colors: { "editor.background": themeConfig.background.color, }, diff --git a/src/utilities/graphs/graph.typings.ts b/src/utilities/graphs/graph.typings.ts index 5b1e4a0..f8ea049 100644 --- a/src/utilities/graphs/graph.typings.ts +++ b/src/utilities/graphs/graph.typings.ts @@ -11,6 +11,7 @@ export enum GraphEdgeGroup { StoreToType = "store-to-type", TypeToRelation = "type-to-relation", RelationToRelation = "relation-to-relation", + AssignableSourceToRelation = "assignable-sourcee-to-relation", Default = "default", } @@ -27,6 +28,7 @@ export interface GraphEdge { from: string; label?: string; group: GraphEdgeGroup; + dashes?: boolean; isActive?: boolean; } diff --git a/src/utilities/graphs/index.ts b/src/utilities/graphs/index.ts index c128ed1..da74122 100644 --- a/src/utilities/graphs/index.ts +++ b/src/utilities/graphs/index.ts @@ -1,3 +1,3 @@ -export { GraphDefinition, GraphEdge, GraphNode, ResolutionTree } from "./graph.typings"; +export { GraphDefinition, GraphEdge, GraphNode, GraphNodeGroup, GraphEdgeGroup, ResolutionTree } from "./graph.typings"; export { TreeBuilder } from "./related-users-graph"; export { AuthorizationModelGraphBuilder } from "./model-graph"; diff --git a/src/utilities/graphs/model-graph.ts b/src/utilities/graphs/model-graph.ts index 4031c01..542d15b 100644 --- a/src/utilities/graphs/model-graph.ts +++ b/src/utilities/graphs/model-graph.ts @@ -1,19 +1,33 @@ -import type { AuthorizationModel, ObjectRelation, TypeDefinition, Userset } from "@openfga/sdk"; +import { AuthorizationModel, ObjectRelation, RelationMetadata, TypeDefinition, Userset } from "@openfga/sdk"; import { GraphDefinition, GraphEdgeGroup, GraphNodeGroup } from "./graph.typings"; +export type TypeGraphOpts = { showAssignable?: boolean }; + export class AuthorizationModelGraphBuilder { private _graph: GraphDefinition = { nodes: [], edges: [] }; constructor( private authorizationModel: AuthorizationModel, - private store?: { name?: string }, + private store?: { name?: string; id?: string }, ) { this.buildGraph(); } + private static getStoreId(storeName: string) { + return `store|${storeName}`; + } + + private static getTypeId(typeId: string) { + return `type|${typeId}`; + } + + private static getRelationId(typeId: string, relationKey: string) { + return `${typeId}.relation|${relationKey}`; + } + private buildGraph() { - const storeName = this.store?.name || "Store"; - const rootId = `store|${storeName}`; + const storeName = this.store?.name || this.store?.id || "Store"; + const rootId = AuthorizationModelGraphBuilder.getStoreId(storeName); const authorizationModelGraph: GraphDefinition = { nodes: [{ id: rootId, label: storeName, group: GraphNodeGroup.StoreName }], edges: [], @@ -38,6 +52,53 @@ export class AuthorizationModelGraphBuilder { (relationDef.union?.child || []).some((child) => this.checkIfRelationAssignable(child)) ); } + + // Get the sources that can be assignable to a relation + private getAssignableSourcesForRelation( + relationDef: Userset, + relationMetadata: RelationMetadata, + ): { + types: string[]; + relations: string[]; + conditions: string[]; + publicTypes: string[]; + isAssignable: boolean; + } { + const assignableSources: { + types: string[]; + relations: string[]; + conditions: string[]; + publicTypes: string[]; + isAssignable: boolean; + } = { types: [], relations: [], conditions: [], publicTypes: [], isAssignable: false }; + + // If this is not used anywhere, then it's not assignable + if (!this.checkIfRelationAssignable(relationDef)) { + return assignableSources; + } + + const assignable = relationMetadata.directly_related_user_types; + assignable?.forEach((relationRef) => { + // TODO: wildcard and conditions + if (!(relationRef.relation || relationRef.wildcard || (relationRef as any).condition)) { + return; + } + + // TODO: Mark relations as assignable once supported + if (relationRef.relation) { + assignableSources.relations.push( + AuthorizationModelGraphBuilder.getRelationId(relationRef.type, relationRef.relation), + ); + return; + } + + assignableSources.isAssignable = true; + assignableSources.types.push(AuthorizationModelGraphBuilder.getTypeId(relationRef.type)); + }); + + return assignableSources; + } + private addRelationToRelationEdge( typeGraph: GraphDefinition, typeId: string, @@ -45,14 +106,19 @@ export class AuthorizationModelGraphBuilder { toRelation: ObjectRelation, ): void { typeGraph.edges.push({ - from: `${typeId}.relation|${fromRelationKey}`, - to: `${typeId}.relation|${toRelation.relation}`, + from: AuthorizationModelGraphBuilder.getRelationId(typeId, fromRelationKey), + to: AuthorizationModelGraphBuilder.getRelationId(typeId, toRelation.relation!), group: GraphEdgeGroup.RelationToRelation, + dashes: true, }); } - private getTypeGraph(typeDef: TypeDefinition, authorizationModelGraph: GraphDefinition): GraphDefinition { - const typeId = `type|${typeDef.type}`; + private getTypeGraph( + typeDef: TypeDefinition, + authorizationModelGraph: GraphDefinition, + { showAssignable }: TypeGraphOpts = {}, + ): GraphDefinition { + const typeId = AuthorizationModelGraphBuilder.getTypeId(typeDef.type); const typeGraph: GraphDefinition = { nodes: [{ id: typeId, label: typeDef.type, group: GraphNodeGroup.Type }], edges: [{ from: authorizationModelGraph.nodes[0].id, to: typeId, group: GraphEdgeGroup.StoreToType }], @@ -61,19 +127,34 @@ export class AuthorizationModelGraphBuilder { const relationDefs = typeDef?.relations || {}; Object.keys(relationDefs).forEach((relationKey: string) => { - const relationId = `${typeId}.relation|${relationKey}`; + const relationId = AuthorizationModelGraphBuilder.getRelationId(typeId, relationKey); const relationDef = relationDefs[relationKey] || {}; - const hasSelf = this.checkIfRelationAssignable(relationDef); + const assignableSources = this.getAssignableSourcesForRelation( + relationDef, + typeDef.metadata?.relations?.[relationKey] || {}, + ); + const isAssignable = assignableSources.isAssignable; - // If a relation definition does not have self, then we call it a `permission`, e.g. not directly assignable + // If a relation definition does not have this, then we call it a `permission`, e.g. not directly assignable typeGraph.nodes.push({ id: relationId, label: relationKey, - group: hasSelf ? GraphNodeGroup.AssignableRelation : GraphNodeGroup.NonassignableRelation, + group: isAssignable ? GraphNodeGroup.AssignableRelation : GraphNodeGroup.NonassignableRelation, }); - // TODO: Support - 1. AND, 2. BUT NOT, 3. Nested relations + if (showAssignable) { + // TODO: Support assignable relations and wildcards, and conditionals + assignableSources.types.forEach((assignableSource) => { + typeGraph.edges.push({ + from: AuthorizationModelGraphBuilder.getTypeId(assignableSource), + to: relationId, + group: GraphEdgeGroup.AssignableSourceToRelation, + }); + }); + } + + // TODO: Support - 1. AND, 2. BUT NOT, 3. Nested relations, 4. Tuple to Userset typeGraph.edges.push({ from: typeId, to: relationId, group: GraphEdgeGroup.TypeToRelation }); if (relationDef.computedUserset) { this.addRelationToRelationEdge(typeGraph, typeId, relationKey, relationDef.computedUserset);