Skip to content

Commit

Permalink
Merge pull request #35 from eliataylor/eli
Browse files Browse the repository at this point in the history
New permissions matrix format; default permissions config;
  • Loading branch information
eliataylor authored Jan 12, 2025
2 parents 6102652 + 5b25961 commit fc4dab0
Show file tree
Hide file tree
Showing 83 changed files with 5,887 additions and 2,009 deletions.
10 changes: 5 additions & 5 deletions docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

#### Generate your Django models, views, serializers and urls:

`python -m django/generate admin --types=examples/object-fields-nod.csv --output_dir=examples/django/oaexample_app`
`python -m generate admin --types=examples/object-fields-demo.csv --output_dir=examples/django/oaexample_app`

#### Generate your TypeScript types, interfaces and URL patterns:

`python -m django/generate typescript --types=examples/object-fields-nod.csv --output_dir=examples/reactjs/src/object-actions/types/types.ts`
`python -m generate typescript --types=examples/object-fields-demo.csv --output_dir=examples/reactjs/src/object-actions/`

#### Test API and Generate fake data for API

Expand All @@ -21,7 +21,7 @@ All Select options under Fields Types (Column D) in the Object Fields sheet come
If you have special field types, add them here. The checkboxes roughly describe what Fields are support by the different
CMS config builders in this repository.

<a href="docs/images/field-types.png" target="_blank">
<img src="docs/images/field-types.png" alt="Field Types" height="200" />
</a>
<a href="docs/images/field-types.png" target="_blank">
<img src="docs/images/field-types.png" alt="Field Types" height="200" />
</a>

14 changes: 9 additions & 5 deletions docs/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,15 @@ get_machinename() {
MACHINE_NAME=$(get_machinename "$PROJECT_NAME")

# Ensure stack directory exists
STACK_PATH=$(resolve_absolute_path "$SCRIPT_DIR" "$STACK_PATH")

if [[ ! -d "$STACK_PATH" ]]; then
mkdir -p "$STACK_PATH"
echo "Created STACK_PATH: $STACK_PATH"
if [[ "$STACK_PATH" == '.' ]]; then
STACK_PATH=$(pwd)
echo "STACK_PATH set to current directory: $STACK_PATH"
else
STACK_PATH=$(resolve_absolute_path "$SCRIPT_DIR" "$STACK_PATH")
if [[ ! -d "$STACK_PATH" ]]; then
mkdir -p "$STACK_PATH"
echo "Created STACK_PATH: $STACK_PATH"
fi
fi

# Make absolute if relative
Expand Down
31 changes: 21 additions & 10 deletions load-sheets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,29 @@ if [[ ! -f "$TYPES_PATH" ]]; then
exit 1
fi

# Check if permissionspath is a valid file path
# Check if permissions path is a valid file path
permissions_arg=""
if [[ -f "$PERMISSIONS_PATH" ]]; then
permissions_arg="--permissions=\"$PERMISSIONS_PATH\""
permissions_arg="--permissions=$PERMISSIONS_PATH"
else
permissions_arg="--default_perm=IsAuthenticatedOrReadOnly"
fi

# Run the Python scripts to generate files
echo "Building Django with types $TYPES_PATH and permissions $PERMISSIONS_PATH"
python -m generate django --types="$TYPES_PATH" $permissions_arg --output_dir="$STACK_PATH/stack/django/${MACHINE_NAME}_app"

# echo "Building TypeScript with types $TYPES_PATH and permissions $PERMISSIONS_PATH"
python -m generate typescript --types="$TYPES_PATH" $permissions_arg --output_dir="$STACK_PATH/stack/reactjs/src/object-actions/types/"
python -m generate typescript --types="$TYPES_PATH" $permissions_arg --output_dir="$STACK_PATH/stack/databuilder/src/"
python -m generate typescript --types="$TYPES_PATH" $permissions_arg --output_dir="$STACK_PATH/stack/cypress/cypress/support/"
python -m generate typescript --types="$TYPES_PATH" $permissions_arg --output_dir="$STACK_PATH/stack/k6/"
echo "Building Django with types $TYPES_PATH and permissions $PERMISSIONS_PATH"
python -m generate django --types="$TYPES_PATH" --output_dir="$STACK_PATH/stack/django/${MACHINE_NAME}_app"

echo "Building forms with types $TYPES_PATH"
python -m generate forms --types="$TYPES_PATH" --output_dir="$STACK_PATH/stack/reactjs/src/object-actions/forming/forms"

echo "creating types.ts with types $TYPES_PATH and permissions $PERMISSIONS_PATH"
python -m generate typescript --types="$TYPES_PATH" --output_dir="$STACK_PATH/stack/reactjs/src/object-actions/types"
python -m generate typescript --types="$TYPES_PATH" --output_dir="$STACK_PATH/stack/databuilder/src/"
python -m generate typescript --types="$TYPES_PATH" --output_dir="$STACK_PATH/stack/cypress/cypress/support/"
python -m generate typescript --types="$TYPES_PATH" --output_dir="$STACK_PATH/stack/k6/"

echo "creating access.ts and permissions.json with $permissions_arg"
python -m generate permissions-ts $permissions_arg --types="$TYPES_PATH" --output_dir="$STACK_PATH/stack/reactjs/src/object-actions/types"
python -m generate permissions-ts $permissions_arg --types="$TYPES_PATH" --output_dir="$STACK_PATH/stack/databuilder/src/"
python -m generate permissions-ts $permissions_arg --types="$TYPES_PATH" --output_dir="$STACK_PATH/stack/cypress/cypress/support/"
python -m generate permissions-ts $permissions_arg --types="$TYPES_PATH" --output_dir="$STACK_PATH/stack/k6/"
28 changes: 13 additions & 15 deletions src/django/DjangoBuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@


class DjangoBuilder:
def __init__(self, types_path, matrix_path, output_dir):
self.output_dir = output_dir
def __init__(self, output_dir):
self.output_dir = output_dir if output_dir.endswith('/') else f"{output_dir}/"
self.app_name = os.path.basename(output_dir)

self.templates_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + '/templates/django/'
self.templates_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + '/templates/django'

self.global_function = {"models": [], "serializers": [], "views": [], "urls": []}
self.imports = {"admin": ["from django.contrib import admin",
Expand Down Expand Up @@ -69,23 +68,17 @@ def __init__(self, types_path, matrix_path, output_dir):
# TODO: generate CRUD query methods based on Permissions Matrix
# TODO: personalize the CustomPagination class

def build_django(self, types_path, default_perm):
if types_path or not os.path.exists(types_path):
self.json = build_types_from_csv(types_path)
self.build_models()
self.build_serializers()
self.build_viewsets()
self.build_viewsets(default_perm)
self.build_urls()
else:
logger.warning(f'Cannot find Object Types {types_path}')
sys.exit(0)

if matrix_path and os.path.exists(matrix_path):
self.matrix_path = matrix_path
self.build_permissions()
else:
logger.warning(f'Cannot find Permissions Matrix {matrix_path}')
sys.exit(0)

def append_import(self, key, val):
if val not in self.imports[key]:
if isinstance(val, list):
Expand Down Expand Up @@ -199,12 +192,14 @@ def build_serializers(self):
inject_generated_code(outpath, '\n'.join(self.imports["serializers"]), 'SERIALIZER-IMPORTS')
inject_generated_code(outpath, serializers_helpers + "\n" + "\n".join(parts), 'SERIALIZERS')

def build_viewsets(self):
def build_viewsets(self, default_perm):
outpath = os.path.join(self.output_dir, 'views.py')

with open(self.templates_dir + '/view.py', 'r') as fm:
tpl = fm.read().strip()

tpl = tpl.replace('IsAuthenticatedOrReadOnly', default_perm)

serialModelMap = []
searchFieldMap = {}

Expand Down Expand Up @@ -250,8 +245,11 @@ def build_viewsets(self):

inject_generated_code(outpath, "\n".join(parts), 'VIEWSETS')

def build_permissions(self):
matrix = build_permissions_from_csv(self.matrix_path, self.json)
def build_permissions(self, matrix_path, default_perm):

return; # TODO: implement

matrix = build_permissions_from_csv(matrix_path, self.json)
if matrix is None or 'permissions' not in matrix:
return None

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
,,,ROLES,,,,,,,,,,RULES,,
,,,anonymous,authenticated,verified,paid user,admin,rally attendee,city sponsor,city official,rally speaker,rally moderator,"1. ""Add Own"" would mean you own what you create
2. ""Any Others"" would mean you create on behalf of someone else",,
,,,ROLES,,,,,,,,,,,EXPLANATIONS,
,,,anonymous,authenticated,verified,paid user,admin,rally attendee,city sponsor,city official,rally speaker,rally moderator,,"- ""Add Own"" refers to what a user creates and owns
- ""Any Others"" refers to what a user creates on behalf of another user",
,Name,Ownership,,,,,,,,,,,URL Pattern,Alias URL,
,,,,,,,,,,,,,,,
Users,,,,,,,,,,,,,,,
,View List,own,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/users,,
,View List,others,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/users,,
,View Profile,own,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/user/[uid],,
,View Profile,others,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/user/[uid],,
,Sign Up,own,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/user/add,,
,Edit User,own,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/user/[id]/edit,,
,Delete User,own ,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/user/[id]/delete,,
,Block User,others,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/user/[id]/block,,
,View List,others,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,/users,Can users view the list of other user's accounts,
,View Profile,own,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/user/[uid],Can user view their own profile details,
,View Profile,others,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/user/[uid],Can users view other's profile details,
,Add,own,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/user/add,Can users register for their own accounts,
,Add,others,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/user/add,Can users create accounts on other's behalf,
,Sign Up,own,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/user/add,Can users update their own profile,
,Edit User,own,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/user/[id]/edit,Can users update other user's profile,
,Delete User,own ,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/user/[id]/delete,Can users delete their own account,
,Block User,others,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/user/[id]/block,Can users delete other another user's account,
,,,,,,,,,,,,,,,
Cities,,,,,,,,,,,,,,,
,View,own,TRUE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/city/[id],,
Expand Down Expand Up @@ -104,13 +105,6 @@ Attendees,,,,,,,,,,,,,,,
,Delete,own ,FALSE,FALSE,TRUE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,/meeting/[id]/user/[id],,
,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,
Pages,,,,,,,,,,,,,,,
,View,own,TRUE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/invites/[id],,
,Add,own,FALSE,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,/invites,,
,Edit,own,FALSE,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,/invites/[id]/edit,,
,Delete,own ,FALSE,FALSE,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,/invites/[id]/delete,,
,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,
Rooms,,,,,,,,,,,,,,,
,View List,,FALSE,FALSE,FALSE,FALSE,TRUE,TRUE,TRUE,TRUE,TRUE,TRUE,/meeting/[id]/rooms,,
,View,own,FALSE,FALSE,TRUE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,FALSE,/rooms/[id],,
Expand Down
21 changes: 12 additions & 9 deletions src/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@

if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate project files based on field types CSV.")
parser.add_argument('command', choices=['django', 'typescript', 'worksheets'],
parser.add_argument('command', choices=['django', 'typescript', 'permissions-ts', 'forms'],
help="Target command for the generation.")
parser.add_argument('--types', required=True, help="Path to the Object Types CSV file.")
parser.add_argument('--permissions', required=False, help="Path to the Permissions Matrix CSV file.")
parser.add_argument('--default_perm', required=False, choices=['AllowAll', 'IsAuthenticated', 'IsAuthenticatedOrReadOnly'], default='IsAuthenticatedOrReadOnly', help="Default permission when matches are not found in permissions matrix")
parser.add_argument('--output_dir', required=True, help="Path to the output directory.")

args = parser.parse_args()
Expand All @@ -22,6 +23,7 @@
types_path = args.types
matrix_path = args.permissions
output_dir = args.output_dir
default_perm = args.default_perm

# TODO: Check if a Google Spreadsheet URL and download programmatically

Expand All @@ -34,20 +36,21 @@
logger.info(f"Creating output directory: '{output_dir}'")

logger.info(f"Running command: {command}")
logger.info(f"Input file: {types_path}")
logger.info(f"Output directory: {output_dir}")

if command == 'worksheets':
pass # clone and build empty worksheets with provided Object Types and basic CRUD URL patterns populated
if command == 'django':
builder = DjangoBuilder(types_path, matrix_path, output_dir)
if matrix_path:
builder.build_permissions()
builder = DjangoBuilder(output_dir)
builder.build_django(types_path, default_perm)
# builder.build_permissions(matrix_path, default_perm)
elif command == 'permissions-ts':
reactor = TypesBuilder(types_path, matrix_path, output_dir)
reactor.build_permissions(default_perm)
elif command == 'forms':
reactor = TypesBuilder(types_path, matrix_path, output_dir)
reactor.build_forms()
elif command == 'typescript':
reactor = TypesBuilder(types_path, matrix_path, output_dir)
reactor.build_types()
if matrix_path:
reactor.build_permissions()


else:
Expand Down
120 changes: 120 additions & 0 deletions src/templates/reactjs/access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { EntityTypes } from "./types";
import permissions from "./permissions.json";

export interface MySession {
id: number;
display: string;
has_usable_password?: boolean;
email: string;
username: string;
picture: string;
groups?: string[];
}

//---OBJECT-ACTIONS-PERMS-VERBS-STARTS---//

export type CRUDVerb = 'view' | 'add' | 'edit' | 'delete';
//---OBJECT-ACTIONS-PERMS-VERBS-ENDS---//

//---OBJECT-ACTIONS-PERMS-ROLES-STARTS---//
export const DEFAULT_PERM: 'AllowAny' | 'IsAuthenticated' | 'IsAuthenticatedOrReadOnly' = 'IsAuthenticatedOrReadOnly';

export type PermRoles = 'anonymous' | 'authenticated' | 'verified';
//---OBJECT-ACTIONS-PERMS-ROLES-ENDS---//


interface AccessPoint {
verb: CRUDVerb;
context: string[]; // a string of EntityType names
ownership: string; // "own" / "any"
id_index?: number;
roles: PermRoles[];
endpoint: string;
alias?: string;
}

function getPermsByTypeAndVerb(type: string, verb: string) {
const matches = (permissions as unknown as AccessPoint[]).filter(
(perms: AccessPoint) => {
return perms.context.join("-") === type && perms.verb === verb; // TODO: permissions.json needs built differently
}
);
return matches;
}

// returns error string or true if passes
export function canDo(
verb: CRUDVerb,
obj: EntityTypes,
me?: MySession | null
): boolean | string {
const perms = getPermsByTypeAndVerb(obj._type, verb);

if (!perms || !perms.length) {
console.warn(`[PERMS] NO MATCHES FOR ${verb} - ${obj._type}`);
if (DEFAULT_PERM === 'AllowAny') return true;
if (me && me?.id > 0 && DEFAULT_PERM === 'IsAuthenticated') return true
if ((verb.indexOf('view') > -1 || verb.indexOf('read') > -1) && DEFAULT_PERM === 'IsAuthenticatedOrReadOnly') return true
return `Default permission Is ${DEFAULT_PERM}`;
}

let isMine = verb === "add";
if (!isMine && me) {
if (obj._type === "Users") {
isMine = obj.id === me.id;
} else {
isMine = "author" in obj && me.id === obj?.author?.id;
}
}

let perm = perms.find(p => p.ownership === "others" && !isMine);
if (!perm) {
perms.find(p => p.ownership === "own" && isMine);
if (!perm) {
perm = perms[0];
console.warn(`[PERMS] MISMATCHED OWNERSHIP isMine: ${isMine}`, perms);
}
}

const myGroups = new Set(
me?.groups && me?.groups.length > 0 ? me.groups : []
);

if (!me) {
myGroups.add("anonymous");
const hasRole = perm.roles.indexOf("anonymous") > -1;
if (hasRole) {
return true;
}
return `Anonymous cannot ${verb} ${obj._type}. You need one of these: ${perm.roles.join(", ")}`;
} else {
myGroups.add("authenticated");
}

let errstr = `You have ${Array.from(myGroups).join(", ")}, but must `;
if (perm.roles.length === 1) {
errstr += ` be ${perm.roles[0]}`;
} else {
errstr += ` have one of these roles - ${perm.roles.join(", ")} - `;
}
errstr += ` to ${verb}`;


if (isMine && perm.ownership === "own") {
if (perm.roles.some((role) => {
return myGroups.has(role) || (role === "authenticated" && me.id) || (role === "anonymous" && !me.id);
})) {
return true;
}
return `${errstr} your own ${obj._type}`;
} else if (!isMine && perm.ownership === "others") {
if (perm.roles.some((role) => {
return myGroups.has(role) || (role === "authenticated" && me.id) || (role === "anonymous" && !me.id);
})) {
return true;
}
return `${errstr} someone else's ${obj._type}`;
}

return `${errstr} ${isMine ? "your own" : "someone else's"} ${obj._type}`;
}
Loading

0 comments on commit fc4dab0

Please sign in to comment.