Skip to content

Commit

Permalink
Merge pull request #377 from amplication/feat/8533-dotnet-auth-plugin
Browse files Browse the repository at this point in the history
Feat(dontet-auth): Add DOTNet authentication plugin
  • Loading branch information
morhag90 authored Jun 3, 2024
2 parents fba87aa + d1f6fef commit 786e932
Show file tree
Hide file tree
Showing 20 changed files with 1,309 additions and 0 deletions.
381 changes: 381 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions plugins/dotnet-auth-core-identity/.amplicationrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"settings": {},
"systemSettings": {
"requireAuthenticationEntity": "true"
}
}
8 changes: 8 additions & 0 deletions plugins/dotnet-auth-core-identity/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": [
"../../.eslintrc.json"
],
"ignorePatterns": [
"!**/*"
]
}
2 changes: 2 additions & 0 deletions plugins/dotnet-auth-core-identity/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.prettierignore
.gitignore
1 change: 1 addition & 0 deletions plugins/dotnet-auth-core-identity/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
14 changes: 14 additions & 0 deletions plugins/dotnet-auth-core-identity/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# @amplication/plugin-dotnet-auth-core-identity

[![NPM Downloads](https://img.shields.io/npm/dt/@amplication/plugin-dotnet-auth-core-identity)](https://www.npmjs.com/package/@amplication/plugin-dotnet-auth-core-identity)

Use authentication in the service generated by Amplication.

## Purpose

This plugin adds the core required code to use authentication in the service generated by Amplication.
It updates the following parts:

- Adds the Authorize attribute in the base controllers by the roles configurations
- Add the required configurations for the auth entity
- Add the required configurations in the program.cs file
37 changes: 37 additions & 0 deletions plugins/dotnet-auth-core-identity/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@amplication/plugin-dotnet-auth-core-identity",
"version": "0.0.1",
"description": "Add Authentication and Authorization to your .NET Services",
"main": "dist/index.js",
"nx": {},
"scripts": {
"dev": "webpack --watch",
"build": "webpack",
"prebuild": "rimraf dist"
},
"author": "Mor Hagbi",
"license": "Apache-2.0",
"devDependencies": {
"@amplication/code-gen-types": "2.0.33-beta.23",
"@amplication/code-gen-utils": "^0.0.9",
"@amplication/csharp-ast": "0.0.3-beta.2",
"@babel/parser": "^7.18.11",
"@babel/types": "^7.18.10",
"@types/lodash": "^4.14.182",
"@types/normalize-path": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"copy-webpack-plugin": "^12.0.2",
"eslint": "^8.21.0",
"jest-mock-extended": "^2.0.7",
"lodash": "^4.17.21",
"pascal-case": "^3.1.2",
"prettier": "^2.6.2",
"rimraf": "^4.4.1",
"ts-loader": "^9.4.2",
"typescript": "^4.9.3",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"@types/pluralize": "^0.0.29"
}
}
6 changes: 6 additions & 0 deletions plugins/dotnet-auth-core-identity/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"targets": {
"lint": {},
"npm:publish": {}
}
}
24 changes: 24 additions & 0 deletions plugins/dotnet-auth-core-identity/src/core/create-app-services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { CodeBlock } from "@amplication/csharp-ast";

export function createAppServices(builderServicesBlocks: CodeBlock[]): void {
builderServicesBlocks.push(
new CodeBlock({
code: `using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
await RolesManager.SyncRoles(services, app.Configuration);
}`,
})
);

builderServicesBlocks.push(
new CodeBlock({
code: `
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
await SeedDevelopmentData.SeedDevUser(services, app.Configuration);
}`,
})
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { CodeBlock, CsharpSupport } from "@amplication/csharp-ast";

export function createBuildersServices(
resourceName: string,
builderServicesBlocks: CodeBlock[]
): void {
builderServicesBlocks.push(
new CodeBlock({
code: `builder.Services.AddApiAuthentication();`,
})
);

const swaggerBuilderIndex = builderServicesBlocks.findIndex((b) =>
b.toString().includes("AddSwaggerGen")
);

if (swaggerBuilderIndex === -1) return;

builderServicesBlocks[swaggerBuilderIndex] = new CodeBlock({
references: [
CsharpSupport.classReference({
namespace: `${resourceName}.APIs`,
name: resourceName,
}),
],
code: `builder.Services.AddSwaggerGen(options =>
{
options.UseOpenApiAuthentication();
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
});`,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Entity, EnumEntityAction } from "@amplication/code-gen-types";
import { CsharpSupport, Method } from "@amplication/csharp-ast/src";
import { getEntityRoleMap } from "./get-entity-roles-map";

export function createMethodAuthorizeAnnotation(
method: Method,
roles: string
): void {
roles &&
method.annotations?.push(
CsharpSupport.annotation({
reference: CsharpSupport.classReference({
name: "Authorize",
namespace: "Microsoft.AspNetCore.Authorization",
}),
argument: `Roles = "${roles}"`,
})
);
}

export function createRelatedMethodAuthorizeAnnotation(
entity: Entity,
entities: Entity[],
fieldPermanentId: string,
method: Method,
methodType: EnumEntityAction,
roles?: string
): void {
const field = entity.fields.find(
(field) => field.permanentId === fieldPermanentId
);

const relatedEntity = entities.find(
(entity) => entity.id === field?.properties?.relatedEntityId
);
if (relatedEntity) {
const rolesMapping = getEntityRoleMap(relatedEntity, roles);
createMethodAuthorizeAnnotation(method, rolesMapping[methodType].roles);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Entity, EnumDataType } from "@amplication/code-gen-types";
import { CodeBlock, CsharpSupport } from "@amplication/csharp-ast";
import { camelCase } from "lodash";
import { pascalCase } from "pascal-case";

export function CreateSeedDevelopmentDataBody(
resourceName: string,
authEntity: Entity,
entities: Entity[]
): CodeBlock {
const { name, pluralName } = authEntity;
const entityNameToCamelCase = camelCase(name);
const entityNamePluralize = pascalCase(pluralName);
const entityFirstLetter = entityNameToCamelCase.slice(0, 1);
return new CodeBlock({
references: [
CsharpSupport.classReference({
name: "Identity",
namespace: "Microsoft.AspNetCore.Identity",
}),
CsharpSupport.classReference({
name: "EntityFrameworkCore",
namespace: "Microsoft.AspNetCore.Identity.EntityFrameworkCore",
}),
CsharpSupport.classReference({
name: resourceName,
namespace: `${resourceName}.Infrastructure`,
}),
],
code: `var context = serviceProvider.GetRequiredService<${resourceName}DbContext>();
var amplicationRoles = configuration
.GetSection("AmplicationRoles")
.AsEnumerable()
.Where(x => x.Value != null)
.Select(x => x.Value.ToString())
.ToArray();
${authEntityDto(authEntity, entities)}
if (!context.${entityNamePluralize}.Any(${entityFirstLetter} => ${entityFirstLetter}.UserName == ${entityNameToCamelCase}.UserName))
{
var password = new PasswordHasher<${name}>();
var hashed = password.HashPassword(${entityNameToCamelCase}, "password");
${entityNameToCamelCase}.PasswordHash = hashed;
var userStore = new UserStore<${name}>(context);
await userStore.CreateAsync(${entityNameToCamelCase});
var _roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
foreach (var role in amplicationRoles)
{
await userStore.AddToRoleAsync(${entityNameToCamelCase}, _roleManager.NormalizeKey(role));
}
}
await context.SaveChangesAsync();`,
});
}

const authEntityDto = (authEntity: Entity, entities: Entity[]): string => {
const { fields } = authEntity;
let codeBlock = "";

for (const field of fields) {
const fieldNamePascalCase = pascalCase(field.name);

if (field.dataType == EnumDataType.Lookup) {
const relatedEntity = entities.find(
(entity) => entity.id === field.properties?.relatedEntityId
);

const relatedEntityFieldName = pascalCase(field.name);

if (field.properties?.allowMultipleSelection) {
// the "many" side of the relation
codeBlock =
codeBlock +
`${fieldNamePascalCase} = model.${relatedEntityFieldName}.Select(x => new ${relatedEntity?.name}IdDto {Id = x.Id}).ToList(),\n`;
} else {
if (field.properties.fkHolderName === authEntity.name) {
break;
} else {
// the "one" side of the relation
codeBlock =
codeBlock +
`${fieldNamePascalCase} = new ${relatedEntity?.name}IdDto { Id = model.${fieldNamePascalCase}Id},\n`;
}
}
} else {
codeBlock =
codeBlock + `${fieldNamePascalCase} = model.${fieldNamePascalCase},\n`;
}
}

return `var ${camelCase(authEntity.name)} = new ${authEntity.name}
{
${codeBlock}
};`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ClassReference, CodeBlock } from "@amplication/csharp-ast";
import { FileMap, IFile, dotnetTypes } from "@amplication/code-gen-types";
import { readFile } from "fs/promises";
import { pascalCase } from "pascal-case";

export async function createStaticFileFileMap(
destPath: string,
filePath: string,
context: dotnetTypes.DsgContext,
classReference?: ClassReference
): Promise<FileMap<CodeBlock>> {
const fileMap = new FileMap<CodeBlock>(context.logger);

if (!context.resourceInfo) return fileMap;
const resourceName = pascalCase(context.resourceInfo.name);
let fileContent = await readFile(filePath, "utf-8");
fileContent = fileContent.replace("ServiceName", resourceName);

const file: IFile<CodeBlock> = {
path: destPath,
code: new CodeBlock({
code: fileContent,
references: classReference && [classReference],
}),
};

fileMap.set(file);
return fileMap;
}
57 changes: 57 additions & 0 deletions plugins/dotnet-auth-core-identity/src/core/get-entity-roles-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
Entity,
EnumEntityAction,
EnumEntityPermissionType,
} from "@amplication/code-gen-types";

export function getRelatedFieldRolesMap(
entity: Entity,
entities: Entity[],
fieldPermanentId: string,
roleNames?: string
): Record<
EnumEntityAction,
{
roles: string;
}
> | null {
const field = entity.fields.find(
(field) => field.permanentId === fieldPermanentId
);

const relatedEntity = entities.find(
(entity) => entity.id === field?.properties?.relatedEntityId
);
if (relatedEntity) {
return getEntityRoleMap(relatedEntity, roleNames);
}
return null;
}

export function getEntityRoleMap(
entity: Entity,
roleNames?: string
): Record<
EnumEntityAction,
{
roles: string;
}
> {
return Object.fromEntries(
entity.permissions.map((permission) => {
return [
permission.action,
{
roles:
permission.type === EnumEntityPermissionType.AllRoles
? roleNames
: permission.type === EnumEntityPermissionType.Granular
? permission.permissionRoles
.map((role) => role.resourceRole.name)
.join(",")
: null,
},
];
})
) as unknown as Record<EnumEntityAction, { roles: string }>;
}
12 changes: 12 additions & 0 deletions plugins/dotnet-auth-core-identity/src/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export {
createMethodAuthorizeAnnotation,
createRelatedMethodAuthorizeAnnotation,
} from "./create-method-authorize-annotation";
export {
getEntityRoleMap,
getRelatedFieldRolesMap,
} from "./get-entity-roles-map";
export { createStaticFileFileMap } from "./create-static-file-map";
export { createBuildersServices } from "./create-builders-services";
export { createAppServices } from "./create-app-services";
export { CreateSeedDevelopmentDataBody } from "./create-seed-development-data";
Loading

0 comments on commit 786e932

Please sign in to comment.