Skip to content

Commit

Permalink
chore: change tokens and wip conditional support (#173)
Browse files Browse the repository at this point in the history
* chore: change tokens and wip conditional support

* chore: run prettier

* feat(consitions): add basic suggestions

* fix: export GraphNodeGroup and GraphEdgeGroup
  • Loading branch information
rhamzeh authored Nov 3, 2023
1 parent 4a6f5c3 commit 82e8d12
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 39 deletions.
2 changes: 2 additions & 0 deletions src/constants/keyword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
50 changes: 28 additions & 22 deletions src/theme/theme.typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions src/theme/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const tokenTypeMap: Record<OpenFgaDslThemeToken, OpenFgaDslThemeTokenType> = {
[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,
Expand All @@ -19,6 +20,9 @@ const tokenTypeMap: Record<OpenFgaDslThemeToken, OpenFgaDslThemeTokenType> = {
[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,
Expand All @@ -28,6 +32,8 @@ const tokenTypeMap: Record<OpenFgaDslThemeToken, OpenFgaDslThemeTokenType> = {
[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(
Expand Down
22 changes: 20 additions & 2 deletions src/tools/monaco/language-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ export function getLanguageConfiguration(monaco: typeof MonacoEditor): MonacoEdi
brackets: [
["[", "]"],
["(", ")"],
["{", "}"],
],
autoClosingPairs: [
{ open: "[", close: "]" },
{ open: "(", close: ")" },
{ open: "{", close: "}" },
],
surroundingPairs: [
{ open: "[", close: "]" },
Expand Down Expand Up @@ -54,6 +56,7 @@ export const language = <MonacoEditor.languages.IMonarchLanguage>{
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: {
Expand Down Expand Up @@ -81,7 +84,7 @@ export const language = <MonacoEditor.languages.IMonarchLanguage>{
[
"@brackets",
"@whitespace",
"type.type-restrictions.value",
OpenFgaDslThemeToken.VALUE_TYPE_RESTRICTIONS_TYPE,
"@whitespace",
OpenFgaDslThemeToken.DELIMITER_COMMA_TYPE_RESTRICTIONS,
],
Expand Down Expand Up @@ -112,7 +115,7 @@ export const language = <MonacoEditor.languages.IMonarchLanguage>{
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],
Expand All @@ -121,6 +124,19 @@ export const language = <MonacoEditor.languages.IMonarchLanguage>{
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)/),
[
Expand Down Expand Up @@ -162,6 +178,8 @@ export const language = <MonacoEditor.languages.IMonarchLanguage>{
[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 },
Expand Down
16 changes: 16 additions & 0 deletions src/tools/monaco/providers/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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 = [
Expand Down
2 changes: 1 addition & 1 deletion src/tools/monaco/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
2 changes: 2 additions & 0 deletions src/utilities/graphs/graph.typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}

Expand All @@ -27,6 +28,7 @@ export interface GraphEdge {
from: string;
label?: string;
group: GraphEdgeGroup;
dashes?: boolean;
isActive?: boolean;
}

Expand Down
2 changes: 1 addition & 1 deletion src/utilities/graphs/index.ts
Original file line number Diff line number Diff line change
@@ -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";
107 changes: 94 additions & 13 deletions src/utilities/graphs/model-graph.ts
Original file line number Diff line number Diff line change
@@ -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: [],
Expand All @@ -38,21 +52,73 @@ 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,
fromRelationKey: string,
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 }],
Expand All @@ -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);
Expand Down

0 comments on commit 82e8d12

Please sign in to comment.